Using CSS to Enforce Accessibility

I am a big proponent of the First Rule of ARIA (don’t use ARIA). But ARIA brings a lot to the table that HTML does not, such as complex widgets and state information that HTML does not have natively.

A lot of that information can be hidden to the average developer unless they are checking the accessibility inspectors across browsers or testing with a suite of screen readers. This is one of the reasons I always advocate for making your styles lean on structure and attributes from the DOM, to make it easier to spot when something is amiss.

It is also, in my opinion, a way to reduce the surface area for accessibility bugs. If your sort icon orientation is keyed to the ARIA property that conveys that state programmatically, then getting that ARIA property wrong will have two effects — a broken interface and a weird icon. Addressing the accessibility issue will fix the visual style.

CSS as Accessibility Detector

None of this is new. Others have talked, written, and coded about it. I have taken it for granted since attribute selector support became widespread in the first decade of the 21st century. But I regularly work with folks who do not come at CSS from the same place I do.

Instead of preaching concepts, let me toss some examples out. Each of these links to a blog post where I explain the styles, how the selector is keying off the HTML, what WCAG Success Criteria they help support, and if there are any gotchas. Please read at least the linked sections of those posts for more detail.


In my post Under-Engineered Responsive Tables I use the following CSS to ensure content is not clipped unless it will be accessible to keyboard users, without creating a confusing mess for screen reader users:

[role="region"][aria-labelledby][tabindex] {
  overflow: auto;

[role="region"][aria-labelledby][tabindex]:focus {
  outline: .1em solid rgba(0,0,0,.1);


In Disclosure Widgets, the following CSS only hides content when the programmatic state of the trigger (assuming it is the previous sibling) has been correctly set to convey the content it controls is hidden:

button[aria-expanded="false"] + * {
  display: none;

button[aria-expanded] + * {
  /* Don't need anything here. */

I use a similar approach in my post Under-Engineered Dependency Questions. Choosing a radio button for “Other” in a set of options shows a text box, provided your HTML structure is simple. An ID selector with a couple pseudo-classes and a general sibling combinator can go a long way:

#p5:not(:checked) ~ div {
  display: none;

Added, 25 July 2022: With support for :has() finally coming to browsers, I can point to this example I use in the accordion for my Disclosure Widgets post:

h2:has(> button[aria-expanded="false"]) + div {
  display: none;

Essentially the container that follows the heading goes away when the button in the heading says it is not expanded.


When writing about Sortable Table Columns, I do not go into detail in the CSS but the selectors are pretty clear in their reliance on the sort property:

[aria-sort="ascending"] > button svg.asc {
  stroke: var(--col-header-color);
  fill: var(--col-header-color);

[aria-sort="descending"] > button svg.des {
  stroke: var(--col-header-color);
  fill: var(--col-header-color);


My Under-Engineered Text Boxen lean on native HTML attributes along with ARIA properties, such as when the state of a control is invalid:

input:not([type="checkbox"]):not([type="file"]):not([type="image"]):not([type="radio"]):not([type="range"])[aria-invalid] {
  background: linear-gradient(135deg, rgba(255,0,0,1) 0, rgba(255,0,0,1) .4em, rgba(255,255,255,1) .4em);


Of course I cover the most common one, showing the current page in a set of navigation links, in Link + Disclosure Widget Navigation:

a[aria-current="page"] {
  background-color: #187aba;
  border-color: #187aba;


For a twist, More Accessible Skeletons uses CSS to spackle over a gap in ARIA support:

*[aria-busy=true], .skeleton[aria-hidden=true] {
  display: none;

What if I Use an Abstraction?

The challenge I run into again and again is with developers who do not know CSS, either at all or very well. Whether they rely on a CSS-in-JS approach, or a class-driven model like Tailwind or BEM, I find they struggle with first understanding the concept of using CSS as I outlined, but then with how to write the syntax in their tooling to make it work.

I am not making fun of Tailwind or BEM (though I may have done that in the past). I do not know either syntax (nor do I know SCSS, SMACSS, SASS, SFPD, and so on) and am not motivated to since my CSS knowledge is older and works everywhere.

I have no answers here, but I decided to ask on Twitter.

I encourage you to read the responses. Much of what I propose above will not work with Tailwind’s structure. Much of the feedback came down to either serving a static CSS file (while watching for layout conflicts) or integrating these styles with an @layer directive to drop it into the place in the generated Tailwind file where you want it.

Phil Wolstenholme started offering feedback immediately and then wrote a post directly after with Tailwind-specific feedback: Tying Tailwind styling to ARIA attributes.

If you use Tailwind, I encourage you to read it (and then maybe leave a comment there or here with feedback). If you do not use Tailwind, maybe some of those concepts can apply in your tools of choice.

Based on the overall responses, and my own digging into BEM, it looks like the selectors I outline above are well outside the scope of what these tools offer. For other tooling, I encourage you to offer your own experience in the comments.


Look, I can’t keep giving you free code. Some day I will be dead.


Many of the global ARIA states (but not properties) are great as styling hooks. Getting familiar with all ARIA states and properties that are available across widgets and other uses can be handy as well.

Every time you come up with a style that reflects a state or property of something (open, closed, expanded, collapsed, on, off, checked, disabled, busy, locked, selected, sobbing uncontrollably), do not use a class. At least not at first. Look at the programmatic change that happens to the underlying HTML that makes that thing happen.

Try to use that as your styling hook instead. Write it in a way where if that change does not happen, neither does your style.


Just as every problem looks like a nail when you are equipped with only a hammer, every layout or styling or widget challenge looks like something you solve with a class when you choose some self-described utility-first CSS tooling. Or JavaScript.

If you use these tools, you still need to know CSS. On top of that, you may need to know the tools’ syntax in order to incorporate any CSS that goes beyond what they offer.

If you have a hand in building these tools, please consider how you can use CSS that promotes and reinforces good and accessible underlying HTML syntax.

If you are building these kinds of tools because you only know how to style things with classes, then maybe go back and learn CSS first. And HTML. And ARIA. And WCAG. And accessibility. And maybe throw in some microdata formats too. Then take that experience into the tool you want to build.



great compilation here, thanks for putting it together.
i especially love the concept of using the styles to “surface” issues that may have been overlooked. adding the styles to the boilerplate seems like a great way to help enforce accessibility across teams whose members have differing levels of interest and proficiency.


This is the crux of it:

Every time you come up with a style that reflects a state or property of something (open, closed, expanded, collapsed, on, off, checked, disabled, busy, locked, selected, sobbing uncontrollably), do not use a class. At least not at first. Look at the programmatic change that happens to the underlying HTML that makes that thing happen.

Nicely done

Leave a Comment or Response

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>