DEV Community

Cover image for Your .active Class Is Lying to Screen Readers
Artclick
Artclick

Posted on

Your .active Class Is Lying to Screen Readers

If I had to guess the single most common code smell in front-end projects, it wouldn't be a missing semicolon or a messy folder structure — it would be this:

.hidden { display: none; }
.disabled { opacity: 0.5; pointer-events: none; }
.active { font-weight: bold; }
.loading { cursor: wait; }
.error { border-color: red; }
Enter fullscreen mode Exit fullscreen mode

Every one of these classes is solving a problem the browser already solved for you, years ago, with a native HTML attribute. We just keep reaching for class="..." out of habit, because that's what we were taught first, and because it feels like "our" code instead of "the platform's" code.

The problem is that a class like .active carries zero built-in meaning. The browser doesn't know it means "this nav item is the current page." A screen reader doesn't know it either. It's just a string you and your CSS have privately agreed on — and that agreement breaks the moment someone renames the class, forgets to update the JS that toggles it, or writes a component that doesn't know the convention exists.

HTML attributes don't have that problem. They're standardized, they're documented, browsers implement real behavior around them, and assistive technology already knows what to do with them. Using them isn't a stylistic preference — it's outsourcing work to the platform instead of reinventing it every project.

Here's a tour of the attributes that should be replacing your classes.

[hidden] instead of .hidden

<div hidden>I'm gone from layout, accessibility tree, everything.</div>
Enter fullscreen mode Exit fullscreen mode

No CSS file required. No risk of some other stylesheet overriding display: none because it had higher specificity. The browser strips it from rendering and from the accessibility tree in one move. If you ever need to override its default behavior (say, an element that needs display: flex when un-hidden), you can still hook into [hidden] directly in CSS — you don't lose any styling power by dropping the class.

[required] instead of .required

<input type="email" required>
Enter fullscreen mode Exit fullscreen mode

This one is a genuine two-for-one: the browser will block form submission and show a native validation message, and you get a free CSS hook via [required] or :invalid. A .required class gives you neither of those — it's purely decorative, and you'd still need JavaScript to actually enforce it. If you want to style the associated <label> too, modern CSS combinators and :has() mean you no longer need a duplicate class there either.

[disabled] instead of .disabled

<button disabled>Save</button>
Enter fullscreen mode Exit fullscreen mode

Same story: real interaction-blocking behavior, announced automatically to screen readers, styleable with [disabled] or :disabled. A .disabled class only changes appearance — the button still receives clicks and focus unless you separately wire up pointer-events and a keyboard trap, which most people forget.

[inert] for whole regions

disabled is great for a single control, but what about a modal's background content, or an off-canvas menu that's currently closed? That's what inert is for — it removes an entire subtree from interaction and focus order in one attribute, instead of you looping through every child and disabling it manually.

<div id="page-content" inert>
  <!-- everything in here is untabbable and unclickable while the modal is open -->
</div>
Enter fullscreen mode Exit fullscreen mode

[role="switch"] instead of .switch

If you're building a toggle, role="switch" (paired with aria-checked) tells assistive tech exactly what kind of control this is and what state it's in — something a .switch class visually implies but never actually communicates.

[aria-current] instead of .active

This is probably the highest-impact swap on this list, because almost every site has a nav bar with a "you are here" indicator.

<a href="/blog" aria-current="page">Blog</a>
Enter fullscreen mode Exit fullscreen mode

aria-current even supports nuance a class can't easily express out of the box — page, step, location, date, time, or true — so a multi-step checkout flow and a paginated breadcrumb can both use the same attribute correctly, just with different values.

[aria-selected="true"] for tabs

For tab interfaces specifically, aria-selected is the more correct signal than aria-current — it tells the accessibility tree which tab in a tablist is active, which is exactly what your .active class was trying to fake.

[aria-busy="true"] instead of .loading / .skeleton

<section aria-busy="true">
  <!-- skeleton content -->
</section>
Enter fullscreen mode Exit fullscreen mode

This tells assistive technology "don't announce this content yet, it's still updating" — something a purely visual .loading class can never do, no matter how convincing your shimmer animation looks.

[aria-invalid] instead of .error

<input aria-invalid="true">
Enter fullscreen mode Exit fullscreen mode

Pair this with [aria-invalid="true"] in your stylesheet and you get styling and an announcement to screen reader users that something needs their attention — particularly useful for radio groups or checkbox fieldsets where a single red border is easy to miss visually and impossible to detect otherwise.

Why this actually matters (not just "cleaner code")

It's tempting to file this whole idea under "nice to have, ship it later." I'd push back on that a little:

  • Accessibility stops being a separate task. When the state is the accessibility signal, you're not bolting on ARIA after the fact — you get it for free as a side effect of writing normal markup.
  • You delete a category of bugs. "The class got toggled but the JS forgot to update the other one" is a bug that simply can't happen when there's only one source of truth for the state.
  • Your CSS specificity wars shrink. Attribute selectors ([disabled], [aria-current="page"]) sit at the same specificity as a class, so you're not giving anything up on that front — you're just selecting something the browser already knows about instead of something you invented.
  • SEO and tooling benefit too. Crawlers, linters, and browser dev tools all understand native attributes. They have no idea what .is-active-2 means.

Where classes still win

This isn't an argument to delete every class from your codebase tomorrow. Layout, spacing, typography, color themes — none of that has a native HTML equivalent, and classes (or utility frameworks built on them) are still the right tool. The rule of thumb I'd suggest: if you're expressing a state, reach for an attribute first; if you're expressing appearance with no semantic meaning, a class is still fine.

Next time you catch yourself typing class="active" or class="disabled", pause for a second and ask whether HTML already has a word for what you're describing. Most of the time, it does.


We're ArtClick, a web development agency based in Kyoto. We build company websites, WordPress sites, and custom systems — with a focus on sites that are fast, well-designed, and easy to maintain long-term. Learn more at artclickdev.com.

Top comments (0)