Back to blog

Accessibility Meets Analytics

4 min read

Cover image for Accessibility Meets Analytics
Image crafted by robots

Analytics has a habit of leaking implementation details.

You ship data-track-name. You ship btn-324. You ship “modal-close-x”.

Then someone refactors the UI and your events turn into archaeology.

I ran into this while working on click tracking. At the same time, we were pushing hard on accessibility. I wanted one approach that helped both goals.

That is when I found a better primitive.

Use the same thing assistive tech uses.

Use the accessible name.

The problem

Typical click tracking reaches for IDs or custom attributes.

window.addEventListener("click", (event) => {
console.log(event.target.id); // "btn-324" or "login-button"
});

Or it hardcodes selectors and event names.

<script>
window.addEventListener("load", () => {
document.querySelectorAll(".download-link").forEach((item) => {
item.addEventListener("click", () => {
fathom.trackEvent("file download");
});
});
});
</script>

Or it bakes analytics into markup.

<button class="plausible-event-name=Button+Click">Click Me</button>

All of these “work”.

They also create the same long term failure mode.

Look at the difference in logs.

"btn-324", "modal-close-x", "nav-item-2", "cta-primary"
"Search products", "Close dialog", "About us", "Start free trial"

One tells you how the UI is built.

The other tells you what the user did.

So how do you get the second set without hand-labeling everything twice?

The better source of truth

Accessible names come from the accessibility tree.

Screen readers use them. Testing Library uses them. They come from standard rules, not your naming conventions.

Accessible names are computed from places like:

Example:

<button aria-label="Search the site">
<svg aria-hidden="true"></svg>
</button>

The computed accessible name is Search the site.

That name is:

Testing Library has a line that stuck with me:

The more your tests resemble the way your software is used, the more confidence they can give you.

Analytics should follow the same principle.

If a user clicks “Search”, your analytics should say “Search”.

Not “btn-324”.

The missing piece

Testing Library computes accessible names using dom-accessibility-api.

That library exposes what we need:

computeAccessibleName(element) -> string

So the approach is simple:

  1. Listen for clicks
  2. Compute the accessible name for the clicked element
  3. Send that string to your analytics tool

Examples

This works with anything. Vanilla JS, React, Astro, Vue, Svelte.

It is not framework-specific. It is DOM-specific.

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)) return;
const name = computeAccessibleName(event.target);
if (!name) return;
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)) return;
const name = computeAccessibleName(event.target);
if (!name) return;
fathom.trackEvent(`${name} clicked`);
}
window.addEventListener("click", handleClick);
});
</script>
</head>
<body>
<button>Search</button>
</body>
</html>
Note

This only works if your UI is labeled. If computeAccessibleName returns an empty string, that is not an analytics bug. That is an accessibility bug.

That is part of the point.

Performance

This looks scary until you remember when it runs.

It runs on clicks.

Not on render. Not on scroll. Not on a timer.

In practice, computeAccessibleName does some DOM traversal, then returns a string. That cost is usually noise compared to the rest of an interaction.

If you do have a high-volume UI with rapid repeated clicks, you can add small optimizations:

You probably do not need any of that on day one.

Why this approach matters

This gives you benefits that are hard to get any other way.

It also surfaces real issues.

If your click logs show empty strings, you have unlabeled controls.

If your logs show five different “Submit” clicks, you have duplicate names that need context.

One fix is aria-labelledby tied to nearby content.

Instead of “Edit”, you can get “Edit Project Alpha” or “Edit User Profile”.

Now your analytics becomes precise without inventing tracking names.

Closing

Analytics should tell you what users did.

Accessibility already defines the language for that.

So stop tracking implementation details.

Track the accessible name.

You get cleaner data, fewer refactor breakages, and a UI that is harder to ship unlabeled.

That is a real win-win.

Let's Discuss

Questions or feedback? Send me an email.

Last updated on

Back to blog