reCAPTCHA WAF Session Token
Programming Languages

The “Other” C in CSS

Thank you for reading this post, don't forget to subscribe!

I think it’s worth listening to anything Sara Soueidan has to say. That’s especially true if she’s speaking at an event for the first time in four years, which was the case when she took the stage at CSS Day 2024 in Amsterdam. What I enjoy most about Sara is how she not only explains the why behind everything she presents but offers it in a way that makes me go “a-ha!” instead of “oh crap, I’m doing everything wrong.”

Sara’s presentation, “The Other ‘C’ in CSS”, was published on YouTube just last week. It’s roughly 55 minutes of must-see points on the various ways CSS can, and does, impact accessibility. I began watching the presentation casually but quickly fired up a place where I could take thorough notes once I found myself ooo-ing and ahhh-ing along.

So, these are the things I took away from Sara’s presentation. Let me know if you’ve also taken notes so we can compare! Here we go, there’s a lot to take in.

Here’s the video

Yes, CSS affects accessibility

CSS changes more than the visual appearance of elements, whether we like it or not. More than that, its effects cascade down to HTML and the accessibility tree (accTree). And when we’re talking about the accTree, we’re referring to a list of objects that describes and defines accessible information about elements.

There are typically four main bits of info about an accTree object:

  • Role: what kind of thing is this? Most HTML elements map to ARIA roles, but not all of them.
  • Name: identifies the element in the user interface.
  • Description: how do we further describe the thing?
  • State: what is its current state? Announce it!

The browser provides interactive features — like checking a checkbox that updates and exposes the element’s information — so the user knows what happens following an interaction.

Accessibility tree objects may also contain properties and relationships, such as whether it is part of a group or labeled by another element.

Example: List semantics

CSS can affect an object’s accessible role, name, description, or even whether it is exposed in the accTree at all. As such, it can directly impact the screen reader announcement. We shared a while back how removing list-style affects list semantics, particularly in the case of Safari, and Sara explains its nuances.

<code>/* Removes list role semantics in Safari */
/* Need to add aria-role=list */
ul {
  list-style: none;
}

/* Does not remove role semantics in Safari */
nav ul {
  list-style: none:
}

/* Removed unless specifically re-added in the markup */
ul:where([role="list"]) {
  list-style: none;
}

/* Preserves list semantics */
ul {
  list-style: "";
}</code>

display: contents

CSS can completely remove the presence of an element from the accessibility tree. I took a screenshot from one of Sara’s slides but it’s just so darn helpful that I figured putting the info in a table would be more useful:

Exposed to a11y APIs? Keyboard accessible? Visually accessible (rendered)? Children exposed to a11y APIs?
display: none
visibility: hidden
opactity: 0 and filter: opacity(0)
clip-path: inset(100%)
position(off-canvas)
.visually-hidden
display: contents

The display: contents method does more than it’s supposed to. In short, we know that display controls the type of box an element generates. A value of none, for example, generates no box.

The contents value is sort of like none in that not box is generated. The difference is that it has no impact on the element’s children. In other words, declaring contents does not remove the element or its child elements from the accTree. More than that, there’s a current bug report saying that declaring contents in Firefox breaks the anchoring effect of an ID attribute attached to an element.

Eric Bailey says that using display: contents is considered harmful. If using it, the recommendation is to set it on a generic

instead of a semantically meaningful element. If we were to use it on a meaningful interactive element, it would be removed from the accTree, and its children would be bumped up to the next level in the DOM.

Visually hiding stuff

Many, many of us use some sort of .visibility-hidden class as a utility for hiding elements while allowing screenreaders to pick them up and announce the contents. TPGi has a great breakdown of the technique.

<code>.visually-hidden:not(:focus):not(:active) {
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0); /* for IE only */
  clip-path: inset(50%);
  position: absolute;
  white-space: nowrap;
}</code>

This is super close to what I personally use in my work, but the two :not() statements were new to me and threw me for a loop. What they do is make sure that the selector only applies when the element is neither focused nor activated.

It’s easy to slap this class on things we want to hide and call it a day. But we have to be careful and use it intentionally when the situation allows for us to hide but still announce an element. For example, we would not want to use this on interactive elements because those should be displayed at all times. If you’re interacting with something, we have to be able to see it. But for generic text stuff, all good. Skip to content links, too.

There’s an exception! We may want an animated checkbox and have to hide the native control’s appearance so that it remains hidden, even though CSS is styling it in a way that it is visible. We still have to account for the form control’s different states and how it is announced to assistive tech. For example, if we hide the native checkbox for a custom one by positioning it way off the screen, the assistive tech will not announce it on focus or activation. Better to absolutely position the checkbox over the custom one to get the interactive accessibility benefits.

Bottom line: Ask yourself whether an interactive element will become visible when it receives focus when deciding whether or not to use a .visually-hidden utility.

CSS and accessible names

The browser follows a specific process when it determines an element’s accessible name (accName):

  • First, it checks for aria-labelledby. If present, and if the ID in the attribute is a valid reference to an element on the page, it uses the reference’s element’s computed text as the element’s accessible name.
  • Otherwise, it checks for aria-label.
  • Otherwise, unless the element is marked with role="presentation" or role="none" (i.e., the element does not accept an accName anymore), the browser checks if the element can get its own name, which could happen in a few ways, including:
    • from an HTML elemnenty, such as alt or title (which is best on an