Track button clicks without Google Tag Manager

Count the click.
Skip the container.

To track button clicks without Google Tag Manager, give the button a stable handle, such as an id or a data attribute, and point your analytics at it with a CSS selector. GA4 records only two kinds of click by itself, outbound links and file downloads; any other button needs a Tag Manager trigger or a line of gtag code. Builderify’s User Journey takes the selector directly: a “Clicks an element” goal counts a click on the element or anything inside it, and data-bdf-goal on the button does it with no settings. Below: what GA4 catches on its own, the Tag Manager route, a selector maker that tests your own button’s HTML, and how to check it works.

By the Builderify team

Illustration · A click on the icon inside a button, seen by two listeners.
01 / What GA4 tracks

Does GA4 track button clicks by itself?
Two kinds.

GA4’s enhanced measurement, on by default for a web stream, records a click event when a link “leads from the current domain to another website”, and a file_download when a link points to a file with a common extension. Those are the only clicks it sees.

A “Book a class” button that links inside your own site, an “Add to basket” that stays on the page, a button that opens a popup: none of them makes an event. For those you define one, through Tag Manager or with gtag code, which is the next section.

What counts as a file, per Google
pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma
One page, six clicks GA4 records
  • Buy glaze from our partner
    https://partner.example/glaze
    click outbound: true

    A link to another domain. Links to domains in your cross-domain list don’t count.

  • Download the timetable
    /files/timetable.pdf
    file_download file_extension: pdf

    A link to a file with a common extension: pdf, docx, xlsx, csv, zip, mp4 and the rest of Google’s list.

  • Book a class
    /book
    nothing

    A link within your own site. GA4 records the next page_view, not which of the page’s links got the click.

  • Add to basket
    nothing

    A button that stays on the page. Nothing records it unless you define an event.

  • Open the size guide
    nothing

    A button that opens a popup or a menu. Same: nothing.

  • Subscribe
    submits a form
    form_submit form_id, form_name

    Form interactions, if switched on: form_start at the first touch in a session, form_submit when it’s sent. It’s the form, not the button.

Illustration · From Google’s list of enhanced measurement events.

Everything enhanced measurement collects

page_view
Page views

Every page load or history change. Always on.

scroll
Scrolls

Once per page, the first time a visitor reaches 90% of its depth.

click
Outbound clicks

A click on a link that leads to another domain.

view_search_results
Site search

A results page, recognised by q, s, search, query or keyword in the URL.

video_start, _progress, _complete
Video engagement

Embedded YouTube videos with JavaScript API support.

file_download
File downloads

A click on a link to a file with a common extension.

form_start, form_submit
Form interactions

First interaction with a form in a session, and its submission.

your_event
Every other click

Defined by you, in Tag Manager or in code. GA4 won’t guess which buttons matter.

02 / With Tag Manager

How to track button clicks in GA4
with Tag Manager.

The usual route: a click trigger finds the button, a GA4 event tag sends the event. Google’s own example triggers on Click Text; a CSS selector holds up better when the wording changes.

  1. 01

    Switch on the click variables

    In Variables, under Built-In Variables, click Configure and tick the Clicks group: Click Element, Click Classes, Click ID, Click Target, Click URL and Click Text. Tag Manager fills in only the ones that are switched on.

  2. 02

    Make a click trigger

    New trigger, type Click – All Elements (any element) or Click – Just Links (only <a> links). Choose Some Clicks and set the condition: Click Element, matches CSS selector, your selector.

  3. 03

    Allow for the element under the pointer

    All Elements reports the exact element that was clicked, which is often the icon or the span inside a button. Write the selector as #book-class, #book-class * so a click on anything inside counts as well.

  4. 04

    Add the GA4 event tag

    New tag, type Google Analytics: GA4 Event: your Measurement ID, an event name such as book_class, and the trigger from step 2. Event names start with a letter, use only letters, numbers and underscores, stop at 40 characters, and are case-sensitive.

  5. 05

    Preview, publish, mark it

    Use Preview to watch the trigger fire, find the event in GA4’s DebugView (Admin › Data display › DebugView), then publish the container. To count it as a conversion, star it under Admin › Data display › Events as a key event.

GA4 without Tag Manager: gtag code

before </body>
document.addEventListener('click', function (event) {
  if (event.target instanceof Element && event.target.closest('#book-class')) {
    gtag('event', 'book_class');
  }
}, true);
  • It needs the Google tag (gtag.js) on every page where the button appears.
  • One listener on the document also catches buttons that appear after the page loads, such as those in a popup, which a querySelector(…).addEventListener(…) line doesn’t.
  • Extra parameters, such as the button’s text, appear in reports only after you register them as event-scoped custom dimensions; Google says to allow 24 to 48 hours.
One button, the GA4 wayTag Manager
  • Container snippet on every pageonce
  • Click variables switched ononce
  • Click trigger with a selector1
  • GA4 Event tag1
  • Preview, then publisheach change
  • Key event star in GA41
  • Custom dimension per parameterif used
The same, with Builderify1 goal

Or one attribute on the button. Section 05 has the details.

03 / Selector maker

Find a CSS selector for your button,
and test it before you use it.

Paste your button’s HTML, or a whole section of the page. The maker lists every clickable element, ranks the selectors for the one you pick, counts what each one catches, flags the names a builder generated, and writes the goal, the attribute, the gtag code and the Tag Manager condition.

Right-click the button › Inspect, then right-click the highlighted line › Copy › Copy element (Chrome, Edge) or Copy › Outer HTML (Firefox). Copy a parent a few levels up to see what else a selector would catch.

Or start from an invented example

Found 8 clickable elements, 2 generated-looking names.

comp-lq5x8k2acss-1x2y3z
2 · Pick the button
  • a Northwind Pottery
  • a Classes
  • a Shop
  • a Book a class caught
  • a Download the timetable
  • button Subscribe
  • a Instagram
  • a Email us

caught the chosen selector matches it. via parent it matches something around it, so Builderify counts its clicks too.

3 · Choose a selector
strongest first · × = matches
  1. Strong Tracking attribute ×1 [data-track="hero-book"] Matches only this element in what you pasted.
  2. Good Link address ×1 a[href="/book"] Matches only this element in what you pasted.Breaks if the link’s address changes, even its query string.
  3. Good Classes ×1 a.btn.btn-primary Matches only this element in what you pasted.
  4. Fragile Id ×1 #comp-lq5x8k2a Matches only this element in what you pasted.A Wix-style generated id (comp-…). You didn’t choose it, so nothing keeps it the same.
4 · Paste it
Builderify
Goal › Clicks an element › Which element?
[data-track="hero-book"]
Builderify, no code
The element with data-bdf-goal added
<a id="comp-lq5x8k2a" href="/book" class="btn btn-primary css-1x2y3z" data-track="hero-book" data-bdf-goal="book_a_class">

Then add a Custom event goal named book_a_class; the value rides along.

GA4, no Tag Manager
gtag.js, before </body>
document.addEventListener('click', function (event) {
  if (event.target instanceof Element && event.target.closest('[data-track="hero-book"]')) {
    gtag('event', 'book_a_class');
  }
}, true);
GA4 with Tag Manager
Click – All Elements › Click Element › matches CSS selector
[data-track="hero-book"], [data-track="hero-book"] *

GA4 Event tag, event name book_a_class.

3 elements match. One of them is the element you picked.

+ How the maker decides

It reads the HTML with its own small parser, the same here and on the server, so nothing you paste runs or loads. It ranks, from strong to fragile: data-bdf-goal, ids you’d have chosen, test and tracking attributes (data-testid, data-track and the like), other data attributes, a form control’s name, the link’s address, its accessible name, and classes that aren’t utilities or states. It marks as fragile ids and classes with long numbers or random-looking parts, the prefixes CSS tools use (css-, sc-, jsx-), and builder ids such as comp-, block-yui_ and w-node-. Only when nothing else is unique does it scope by a parent’s id, and then fall back to a position path.

CSS can’t select by text. If wording is all you have, Tag Manager’s Click Text variable can match it, and breaks the day someone rewrites the button.

04 / Selectors that last

A CSS selector for click tracking
that survives a redesign.

A click goal fails quietly. The button still works, the selector just stops matching, and the count drops to zero without an error anywhere. Choose a selector that describes what the button is, not where it sits or how it looks.

  1. 01

    An id you chose

    Ids are unique by definition, and one you typed yourself stays until you change it.

    #book-class #comp-lq5x8k2a
  2. 02

    A data attribute

    It says what the element is for, and no restyle touches it. data-bdf-goal makes the selector unnecessary.

    [data-track="book"]
  3. 03

    Not a generated name

    Hashes and long numbers come from a builder or a build tool. They change when it rebuilds.

    button.css-1x2y3z
  4. 04

    Not a position

    nth-child counts siblings. Add a banner above the button and the selector points at something else.

    div:nth-child(3) > a
  5. 05

    Not a look

    Utility and state classes describe colour and spacing, or whether a menu is open. Restyle the button and they’re gone.

    .bg-red-600.rounded-full
  6. 06

    Wide only on purpose

    A selector that catches every PDF link is a feature. One that catches every button by accident is a bug: scope it with a parent.

    a[href$=".pdf"] .btn
On a website builder
Builder Avoid Use
Squarespace Block ids such as block-yui_3_17_2_1_…: a new block gets a new one, so rebuilding the button breaks the goal. The button’s link, a[href="/book"]. In a code block you write yourself, any id you like.
Wix Ids like comp-… on the published page and generated class names. The ID you set in the editor isn’t the one on the live page, Wix users report. A class of your own, typed into the Custom classes field of the CSS Classes panel, or the link’s address.
Webflow w-node-… ids, and combo classes every similar button shares. Element settings › Custom attributes: add data-bdf-goal and a name. No code at all.
Shopify Ids with long numbers in them, which themes often build from a section id. The product form: a Submits a form goal on form[action*="/cart/add"]. Liquid’s product form posts to /cart/add.
WordPress Classes and ids with numbers that a page builder adds. Block settings › Advanced › Additional CSS class(es). On a Button block the class lands on the wrapper around the link, which a Builderify click goal still catches.
05 / Without GTM

Click tracking without GTM:
one goal, or one attribute.

In Builderify’s User Journey, a click is a goal like any other. Open the goals, add one, choose Clicks an element and paste the selector. Or skip the selector: put data-bdf-goal="booked_class" on the button and make a Custom event goal with the same name. Each completion lands on the visitor’s path, next to the pages that led to it.

Goals have no rate of their own: User Journey costs 0.1 credits per new visitor and 10 per 1,000 page loads, out of 250 free credits a month; see pricing. Counting a thank-you page instead? The guide to conversions by URL covers page goals. The same kind of click goal can also decide an A/B test.

Illustration · After Builderify’s goal editor; hint text quoted from it.

What Builderify does

  • A Clicks an element goal takes a CSS selector. A click on the element or on anything inside it counts: Builderify looks up from the clicked element with closest(), so there’s no , … * to remember
  • One click listener on the whole document, in the capture phase: buttons added after the page loads are covered, and a script that stops the click from bubbling doesn’t hide it
  • The click is sent with sendBeacon, so it arrives even when the button navigates away
  • data-bdf-goal="name" on any element fires the custom event of that name on click, with data-bdf-value as its value when above zero
  • From your own code: builderify('goal', 'name', { value: 49 }). Event names are matched without regard to case
  • Submits a form counts real submissions only, not a press on Send that fails validation: form for any form, or a selector for one
  • Every goal can be limited to some pages, counted every time, once per visit or once per visitor, and given a value
  • A double click within one second counts once

Know this first

  • Click, form and event goals record only while the goal exists. There’s no history to search, so a goal made today starts counting today
  • data-bdf-goal needs a Custom event goal with the same name. Without one, the click goes nowhere
  • The goal editor won’t save a selector with <, >, { or }, or one over 255 characters. That rules out the child combinator: write nav a, not nav > a
  • Only the click event counts. A middle-click or a right-click › Open in new tab isn’t one
  • Buttons inside an iframe, such as an embedded booking or payment widget, belong to another page. No script on yours can see those clicks
  • A form a script sends without a real submit event isn’t seen. Count its thank-you page, or fire a custom event
  • Visitors with Do Not Track on, and those who haven’t allowed analytics when Wait for cookie consent is on, aren’t recorded

Selectors for outbound links, PDF downloads and the rest

Paste any of these into a Clicks an element goal. They also work as a Tag Manager condition, with , … * added for clicks on anything inside.

To count Selector Note
Every PDF download a[href*=".pdf"] *= also catches a link with ?v=2 after the file name.
Every link to another site a[href^="http"]:not([href*="example.com"]) Put your own domain in place of example.com.
Email links a[href^="mailto:"]
Phone links a[href^="tel:"]
WhatsApp chats a[href*="wa.me"] WhatsApp’s click-to-chat links use wa.me.
Any button in one section #pricing a, #pricing button The comma means “or”.

Leaving GA4 altogether? The comparison of simpler Google Analytics alternatives weighs the options, how to replace Google Analytics covers the move, and the guide to user journey tracking shows what a visitor’s path looks like once the clicks are on it. If the button links to another of your domains, read how cross-domain tracking works too.

06 / Test it

How to test click tracking
before you trust the number.

Test the selector on the live page first, then the goal or the tag. Both take a minute, and both catch the mistake that otherwise shows up as a month of zeros.

In the browser’s console, on your live page
document.querySelectorAll('#book-class').length

How many elements the selector catches. 1 for one button; 0 means a typo, or the button is inside an iframe.

$0.closest('#book-class')

Select the icon or text inside the button in the Elements panel first. If this returns the button, a Builderify click goal counts that click.

With Builderify

  1. 1
    Save the goal

    It starts listening the moment you save it.

  2. 2
    Open your site as a visitor would

    With Do Not Track off, and with analytics allowed in the banner if Wait for cookie consent is on.

  3. 3
    Click once

    The completion appears on the goal within seconds.

  4. 4
    Nothing?

    Run the selector in the console (above), check the goal’s page scope, and make sure the button isn’t inside an iframe.

With GA4 and Tag Manager

  1. 1
    Preview in Tag Manager

    Tag Assistant opens your site and lists each click with the triggers it fired, and the values of the click variables.

  2. 2
    Watch DebugView

    Admin › Data display › DebugView shows events from your own browser while debug mode is on.

  3. 3
    Publish the container

    Preview tests only your browser. Nothing reaches visitors until you publish.

  4. 4
    Give parameters time

    A parameter you register as a custom dimension reaches reports 24 to 48 hours later, Google says.

Questions

Button click tracking, before you ask.

Partly. GA4's enhanced measurement records two kinds of click by itself: links to another domain (the click event) and links to files with common extensions (file_download). Any other button, such as one that stays on your site or opens a popup, needs an event you define: a gtag('event', …) call in your page code, or a Tag Manager trigger and tag.

The All Elements trigger reports the exact element under the pointer, which is often an icon or a span inside the button, so a condition like Click Element matches CSS selector #buy misses it. Write it as #buy, #buy * so clicks on anything inside count too. Also check that the click variables are switched on and that the container is published, not only previewed.

An id or a data attribute you added yourself, such as #book-class or [data-track="book"]. Avoid names a builder or build tool generated, like css-1x2y3z, comp-… on Wix or block-yui_… on Squarespace, and avoid positions such as div:nth-child(3), because they change when the page is edited. Test it in the browser console with document.querySelectorAll('your selector').length.

Yes. A Clicks an element goal looks up from the clicked element with closest(), so a click on anything inside the matched element counts, with no extra * selector. It listens on the whole document, so buttons added after the page loads are covered too, and it sends the click even when the button navigates away.

Add data-bdf-goal="your_name" to the button, and optionally data-bdf-value="49", then create a Custom event goal in User Journey with the same name; case doesn't matter. A click on the element or anything inside it fires the event. Without a matching goal nothing is stored, and there is no history to search, so the goal counts from the moment you create it.

Stop losing the visitors
you already paid for.

More leads from the traffic you already have, and follow-up that sends itself. Free to start. No card, no monthly fee.