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
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.
-
Buy glaze from our partnerhttps://partner.example/glazeclick outbound: true
A link to another domain. Links to domains in your cross-domain list don’t count.
-
Download the timetable/files/timetable.pdffile_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/booknothing
A link within your own site. GA4 records the next page_view, not which of the page’s links got the click.
-
Add to basketnothing
A button that stays on the page. Nothing records it unless you define an event.
-
Open the size guidenothing
A button that opens a popup or a menu. Same: nothing.
-
Subscribesubmits a formform_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
Every page load or history change. Always on.
Once per page, the first time a visitor reaches 90% of its depth.
A click on a link that leads to another domain.
A results page, recognised by q, s, search, query or keyword in the URL.
Embedded YouTube videos with JavaScript API support.
A click on a link to a file with a common extension.
First interaction with a form in a session, and its submission.
Defined by you, in Tag Manager or in code. GA4 won’t guess which buttons matter.
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.
-
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.
-
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.
-
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.
-
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.
-
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.
- 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
Or one attribute on the button. Section 05 has the details.
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.
Found 8 clickable elements, 2 generated-looking names.
- 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.
- Strong Tracking attribute ×1 [data-track="hero-book"] Matches only this element in what you pasted.
- 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.
- Good Classes ×1 a.btn.btn-primary Matches only this element in what you pasted.
- 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.
[data-track="hero-book"]
<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.
document.addEventListener('click', function (event) {
if (event.target instanceof Element && event.target.closest('[data-track="hero-book"]')) {
gtag('event', 'book_a_class');
}
}, true);
[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.
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.
-
01
An id you chose
Ids are unique by definition, and one you typed yourself stays until you change it.
#book-class #comp-lq5x8k2a -
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"] -
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 -
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 -
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 -
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
| 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. |
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.
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, withdata-bdf-valueas 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:
formfor 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-goalneeds 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: writenav a, notnav > 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.
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.
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
Save the goal
It starts listening the moment you save it.
-
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
Click once
The completion appears on the goal within seconds.
-
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
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
Watch DebugView
Admin › Data display › DebugView shows events from your own browser while debug mode is on.
-
3
Publish the container
Preview tests only your browser. Nothing reaches visitors until you publish.
-
4
Give parameters time
A parameter you register as a custom dimension reaches reports 24 to 48 hours later, Google says.
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.
Sources
Read on 14 October 2026.
- Analytics Help [GA4] Enhanced measurement events
- Analytics Help [GA4] Event naming rules
- Analytics Help [GA4] Event collection limits
- Analytics Help [GA4] Create event-scoped custom dimensions
- Analytics Help Mark events as key events
- Analytics Help Monitor events in DebugView
- Google for Developers Set up events (gtag.js)
- Tag Manager Help Click trigger
- Tag Manager Help Built-in variables for web containers
- Tag Manager Help Regex and CSS selector operators
- Tag Manager Help Set up Google Analytics events in Tag Manager
- Tag Manager Help Universal Analytics events [Legacy]: the Built-In Variables Configure step
- Simo Ahava #GTMTips: Use wildcard CSS selectors with All Elements triggers (2018)
- MDN Element: closest() method
- MDN Element: click event
- Webflow Help Custom attributes
- Wix Developers Apply custom CSS styling
- Wix Studio Community Generated comp- ids on live pages (user reports, 2025)
- Will Myers What is a block id in Squarespace
- Shopify Dev Liquid tags: form
- WordPress.org Advanced settings overview
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.