I’ve seen analytics setups that rely on data attributes like data-track-name
or plain IDs. They’re brittle, break during refactors, and require tedious upkeep, which is a constant source of pain for developers. In one project, we were also on a mission to improve accessibility, so I wanted a solution that aligned with that goal.
That’s when I discovered a better way. While writing tests using Testing Library, I wondered if we could leverage the same principles for analytics. After some research, I found the dom-accessibility-api
library, which can compute accessible names for any DOM element.
This approach has transformed how we handle click tracking. Here’s how it works and why it’s a game-changer.
The Problem
Typical click tracking looks something like this:
window.addEventListener("click", (event) => { console.log(event.target.id); // "btn-324" or "login-button"});
or with fathom analytics:
<script> window.addEventListener("load", (event) => { document.querySelectorAll(".download-link").forEach((item) => { item.addEventListener("click", (event) => { fathom.trackEvent("file download"); }); }); });</script>
or with Plausible:
<button class="plausible-event-name=Button+Click">Click Me</button>
This works, but it tells you very little about what the user did. IDs and custom properties often change during refactors, and they don’t always match what’s visible on screen. Analytics ends up filled with mystery strings that don’t help product teams understand real behavior. This also puts the burden on developers to maintain these attributes, which can lead to inconsistencies and errors.
Lastly, it doesn’t encourage good accessibility practices, since there’s no connection between what’s tracked and what users actually experience.
Compare these analytics results:
"btn-324", "modal-close-x", "nav-item-2", "cta-primary""Search products", "Close dialog", "About us", "Start free trial"
The difference is immediately clear: one reveals code internals, the other captures user intent.
So how do we make analytics match what users actually experience? The answer lies in the accessibility tree.
Leveraging Accessible Names
Accessible names come from the accessibility tree, which is built for assistive technologies like screen readers. They follow a clear set of rules, pulling labels from:
aria-label
aria-labelledby
<label>
elements- visible text content
alt
attributes on images
Example:
<button aria-label="Search the site"> <svg aria-hidden="true"></svg></button>
Would result in a computed accessible name of Search the site
. This name is reliable, descriptive, and already present in your UI.
Testing Library promotes:
The more your tests resemble the way your software is used, the more confidence they can give you.
To accomplish this, it provides queries such as getByRole
that use accessible names to find elements. The same logic applies perfectly to analytics.
Behind the scenes, Testing Library uses the dom-accessibility-api
library to compute these names. The library follows W3C’s rules for computing accessible names, the same logic that assistive technologies like screen readers rely on.
dom-accessibility-api
provides a computeAccessibleName
function that takes a DOM element and returns its accessible name as a string. That is exactly what we need for meaningful analytics.
Examples
Since dom-accessibility-api
works in any environment, you can use it with plain JavaScript, React, Vue, Svelte, or whatever you prefer.
React
Here’s the bare minimum example using React:
import { useEffect } from "react";import { computeAccessibleName } from "dom-accessibility-api";
export default function App() { useEffect(() => { function handleClick(event: MouseEvent) { if (event.target instanceof Element) { const name = computeAccessibleName(event.target);
if (name) { // or your analytics provider of choice fathom.trackEvent(`${name} clicked`); } } }
window.addEventListener("click", handleClick);
return () => window.removeEventListener("click", handleClick); }, []);
return <button>Search</button>;}
Astro
---import { computeAccessibleName } from "dom-accessibility-api";---
<html lang="en"> <head> <script> window.addEventListener("load", () => { function handleClick(event) { if (event.target instanceof Element) { const name = computeAccessibleName(event.target);
if (name) { // or your analytics provider of choice fathom.trackEvent(`${name} clicked`); } } }
window.addEventListener("click", handleClick); }); </script> </head></html><body> <button>Search</button></body>
This only requires a few lines of code and works across your entire app. You can easily adapt it to log events to your analytics provider of choice.
This approach only works if we’re following good accessibility practices. If a button has no label, it will log an empty string, which is a clear sign of an issue that needs to be fixed.
Before diving into the broader benefits, let’s address a common concern: performance.
Performance Considerations
In practice, computeAccessibleName
is lightweight since it’s only called on user interactions, not continuously. The function performs DOM traversals to compute names, but this happens infrequently compared to other operations in a typical web app. For high-traffic applications, you could implement simple optimizations like caching results for identical elements or debouncing rapid clicks.
Why This Approach Matters
- Resilient: Built on accessibility standards, not brittle DOM selectors.
- User-focused: Matches what people actually perceive and interact with.
- Universal: Compatible with any framework or vanilla JavaScript.
- Self-auditing: Surfaces missing or incorrect labels, driving better accessibility.
- Contextual analytics: Event logs become instantly understandable.
- Improved developer experience: Simple to implement and resistant to UI changes.
- Enhanced collaboration: Designers, PMs, and Engineers all see data in familiar, user-facing language.
In production, this approach has not only improved our analytics quality but also surfaced accessibility issues we didn’t know existed. Buttons with empty accessible names immediately showed up in our click data as empty strings. We also discovered duplicate names, like multiple “Submit” buttons, and missing labels in dynamically loaded content, which made it clear where improvements were needed.
For example, we had multiple “Edit” buttons across different sections that all logged the same name, making it impossible to distinguish which content was being edited. This pushed us to use aria-labelledby
to reference section headings, creating unique names like “Edit Project Alpha” and “Edit User Profile”.
By aligning analytics with accessibility, you gain accurate, meaningful data while improving user experiences. It’s a rare win-win for modern web development.