How Our CSS Framework Helps Enforce Accessibility

Screenshot of two visually identical 'Buy it Now' buttons

Spot the difference….You can’t! To a sighted user it appears we have two identical button elements.

A user interface control not only needs to look like a certain control, it must be described as that control too. Take for example a button, one of the simplest of controls. There are many ways you can create something that looks like a button, but unless you use the actual button tag (or button role – more on roles later), it will not be described as a button.

Why does it need to be described as a button? Users of AT (assistive technology), such as a screen reader, may not be able to see what the control looks like visually; therefore it is the job of the screen reader to describe it aurally. A screen reader, such as VoiceOver for Mac OSX and iOS, can do this job only if we, the developers, ensure the correct semantics are present in our HTML code.


In the table below, compare and contrast the accessibility tree attributes for each element  (hint: click each image to view at full size). VoiceOver uses the accessibility tree to convey to the user what it knows about the web page. You will see that for the fake button, there is nothing in the tree to identify the span element as a button. Quite simply, VoiceOver does not know this element is intended to be a button.

Now spot the difference: this is how VoiceOver sees these two elements
‘REAL’ button ‘FAKE’ button
HTML <button class="btn">Buy it Now</button> <span class="btn">Buy it Now</span>
ACCESSIBILITY TREE ATTRIBUTES Annotated accessibility tree of real button Annotated accessibility tree of fake button
VOICEOVER “Buy it now, Button.”

“To click this button press CTRL-OPTION-SPACE.”

“Buy it now.”

Accessibility tree screenshots taken from Mac OSX Accessibility Inspector

What’s also interesting is that if you look at the ‘Actions’ section of the tree, the real button has an ‘accessibilityPerformPress’ action, while the fake button does not. Armed with this information, VoiceOver can also describe how to interact with the element (e.g., press CTRL-OPTION-SPACE). No such information will be communicated for the fake button.

We can safely say that this fake button is not accessible, because the AT doesn’t know what it is or how to interact with it. It appears our fake button is accessible only to people who can see the screen and use a mouse. Oh dear – this fake button has excluded a large number of our users from being able to buy items!

Swiss cheese

You might be wondering, “Who on earth would use a span or div tag for a button?”

You might now also be thinking, “What on earth does Swiss cheese have to do with any of this?”

In the Swiss cheese model of accident causation, risk of a threat becoming a reality is mitigated by differing layers and types of defenses that are “layered” behind each other. For example, we might use code linting, code reviews, accessibility checkers, and manual testing to help ensure that this button is properly described. We liken these separate layers to multiple slices of Swiss cheese, stacked side by side – hence the name.

Illustration of swiss cheese model

Is there anything cheese can’t do? Although many layers of defense lie between hazards and accidents, there are flaws in each layer that, if aligned, can allow the accident to occur.

What if we could also write our CSS framework in a way that acts as another layer in our line of defense? Read on to find out how!

Enforcing roles

Continuing on from our previous ‘fake button’ example, let’s suppose the developer had created the following rules to make the span element appear visually like a button:

.btn {
  background-color: #0654ba;
  border-radius: 0.25em;
  color: white;
  padding: 0.25em 1em;

Screenshot of fake 'Buy it Now' button

The dreaded fake button (although you still can’t tell, just by looking at it)

What we have here is the proverbial cart before the horse. The developer has styled the element before describing its purpose. One way in which we can create the necessary description (the horse) is to require a role attribute. We’ll go into more detail on the role attribute later, but here’s the interesting bit – we can leverage attribute selectors and re-write our CSS like so:

[role=button].btn {
  background-color: #0654ba;
  border-radius: 0.25em;
  color: white;
  padding: 0.25em 1em;

Screenshot of unstyled 'Buy it Now' span element

Under the skin: our attribute selector has now exposed the fake button for the fraud that it is!

Our selector now ensures that a button will visually appear like a button only if it has first been described as a button. You can almost think of this as TDD (test-driven development). If the HTML does not pass our ‘test’, the visual style will not be applied.

Implicit roles

It’s important to know that nearly all elements have a default implicit role, and these default roles do not need to be specified in the HTML – to do so would be redundant. No prizes for guessing what the default role of a button element is. Yes, it’s button!

You might think that it was easy enough for us to convert a span into an accessible button using the button role, but in actual fact our work would not be finished there. Adding a role does not add behavior. A fully accessible button must be keyboard focusable and it must be invokable with SPACE and ENTER keys too. A button element gives this behavior for free; a span element – even with a role of button – does not, and we must implement its behavior by hand.

So please, and I really can’t emphasize this strongly enough, do everybody a favor and always use an actual button element for buttons.

The only real reason you might have for using the button role is when progressively enhancing a link into a button using JavaScript; for example, to make the link open an overlay instead of a new page – which is exactly what we do on eBay. As with spans and divs, allowing anchor tags for buttons does re-open the door to misuse and abuse (think ‘faux’ buttons); and though it is possible to enforce the correct usage with clever use of attribute selectors, it’s a little more convoluted and therefore beyond the scope of this post.

Again, we can enforce this markup requirement by rewriting our CSS selector like so:

button.btn {
  background-color: #0654ba;
  border-radius: 0.25em;
  color: white;
  padding: 0.25em 1em;

Screenshot of our final 'Buy it Now' button

Horse, cart, & driver: the element now has the appearance, description, and interaction of a button

Finally, no more span and div tags for buttons. Our CSS framework simply does not allow it.

Enforcing states

So far we’ve looked at a simple example of how CSS selectors can force developers to put the proper semantics in place – whether implicitly or explicitly. But what about state? If an element has state (a checked checkbox for example), it is not sufficient to describe only what the element is; we must also describe what state it is in.

Developers often fall into exactly the same trap as before: they convey the state visually but not aurally.

In the following code example, the developer has used a modifier class of btn--disabled in order to alter the opacity and background-color of the button:

button.btn--disabled {
  background-color: #999;
  opacity: 0.5;

Screenshot of a button that appears visually disabled

Our ‘ghosted out’ button appears visually disabled

Modifier class is a BEM (Block, Element, Modifier) concept. Throughout this article we will be using a variation of BEM in order to structure and distinguish our class names.

You might be thinking that this isn’t really disabled. If so, you are quite right. This button will not be described as disabled and it will not behave as disabled.

Again, you might be thinking, “Who actually does this kind of stuff?”, but fear not, our CSS selectors can again protect us from this manner of profanity:

button[disabled] {
  background-color: #999;
  opacity: 0.5;

As you can see, the previous modifier class will no longer cut the mustard. It is removed from the selector entirely and the HTML disabled property takes its place. Only when this property is applied in the markup will the button be well and truly disabled for all users.

Comparing accessibility trees we see that the button with class name is still described as ‘Enabled’
Disabled property Disabled class
Annotated accessibility tree of button with disabled property Annotated accessibility tree of button with disabled classname

So far, none of this is particularly earth-shattering, I’m sure you agree, but it sets the stage nicely for moving onto more complex controls and widgets, where we must start delving deeper into the world of WAI-ARIA (commonly referred to as just ARIA for short).


HTML gives us only a limited set of controls such as buttons, links, and the various form value inputs. What about menus, tabs, carousels, overlays, etc. – how do we describe those? Yes, you guessed it – ARIA comes to our rescue.

ARIA gives us many more roles beyond a simple button, and these roles, in conjunction with a multitude of states and properties, open up a whole new set of desktop-like user interface controls and widgets for us to play with. Just make sure you read the instructions before diving in. You do read the instructions don’t you?

Look out for more controls in HTML5, such as menu and dialog. In fact, you might be interested to know that both the menu and dialog tags started out life as ARIA roles before they were introduced as bona fide HTML elements. Don’t get too excited, though – neither have cross browser support at the time of this writing.

In the next section we will look at an example of such a widget and demonstrate how we can use ARIA to influence the way we write CSS selectors in order to enforce accessible markup.


A tabs widget allows the layered stacking of two or more content panels, whereby only one panel of content can be visible at any time. A list of clickable tabs allows the user to swap out the visible panel. This all happens on the client, without a full page reload (i.e., the client is stateful). By decluttering the user interface in this way we can say that a tabs widget follows the principle of progressive disclosure.

Screenshot of eBay's tabbed interface for sign-in or register

Using tabs, the user can switch between “Sign In” or “Register” without a full page reload.

It is critical that our interface is not only visually identifiable as a tabs control (I’ve seen designs that struggle even to meet this criterion!), but also aurally. Without any tab-related HTML tags, how do we achieve this?

Faux tabs

A seasoned developer might set out initially to create the tabs as a list of clickable page anchors for the tabs, with a group of divs acting as anchor targets for the tab panels:

<div class="tabs">
    <li class="tabs__tab tabs__tab--selected">
      <a href="#sign-in">Sign in</a>
    <li class="tabs__tab">
      <a href="#register">Register</a>
    <div class="tabs__panel tabs__panel--active" id="sign-in">
      <!-- Sign in Content -->
    <div class="tabs__panel" id="register">
      <!-- Register Content -->

This is a perfectly reasonable approach to begin with. Page anchors are often well suited as the starting point for tabs, because in the case of JavaScript being unavailable they ensure at least some basic functionality when clicked (i.e., the browser will scroll to the content of the relevant panel). However, when JavaScript does become available, care must be taken to prevent the default link behavior so as to not interfere with tab semantics and behavior. Let me be very clear about this: links are not the same as tabs!

This technique of making core content and functionality available pre-CSS and pre-JavaScript is called progressive enhancement. Progressive enhancement is the safest and surest way to guard against the unknown (e.g., script timeout, script failure, scripting disabled) and to ensure your core experience remains backwards and forwards compatible in all HTML-capable browsers.

We will assume that all layout-related styling is in place for the links (i.e., they are neatly spaced out horizontally), and that by default the visible state of all panels is hidden, with only the ‘active’ panel displayed. Let’s then suppose our developer chooses to visually convey the selected ‘tab’ state using only an underline (a veritable tour de force of minimalism, I know):

.tabs__tab {
  text-decoration: none;
.tabs__tab--selected {
  text-decoration: underline;
.tabs__panel {
  display: none;
.tabs__panel--active {
  display: block;

It would now take only a small amount of JavaScript for our developer to turn this into a “functioning” tabs widget by preventing the default link action (i.e., prevent it navigating to the URL fragment) and toggling the ‘selected’ and ‘active’ modifier classes accordingly; and indeed our developer might be tempted to stop there.

But although this control looks like a tabs widget, it will currently be described only as a list of links (scroll down to see the accessibility tree). No clues are given as to the dynamic, stateful nature of the widget. Screen reader users attempting to follow one of these links are going to be surprised when nothing happens after invoking the link, and equally surprised when no navigation occurs. They are left guessing as to what type of control they might be interacting with. Not a good experience.

Let’s fix it so that if developers try to use our amazingly awesome CSS to style their tabs like ours (go on, admit it, you want that underline too), the styles will appear only if they have the correct accessible markup in place.

Real tabs

To achieve the correct markup for tabs, just as with our simple button example, we can replace class names with ARIA roles and states.

Luckily, ARIA gives us a set of tab-related roles:

  • tablist
  • tab
  • tabpanel

We can also leverage the following global ARIA states:

  • aria-selected
  • aria-hidden
  • aria-controls
  • aria-labelledby

Whilst it would be entirely possible to continue on with our demonstration of progressive enhancement by applying the above roles and states to override our previous link-based markup, it does add some additional complexities which might distract us from the primary topic at hand. So, rather than getting bogged down in those details, let’s drop the progressive enhancement for now and pretend we live in a magical world where JavaScript is always on, is always available, and always works.

Actually, to be honest, it’s not just a JavaScript issue. Some people would argue that by using list-based markup, we also provide for a reasonable semantic fallback in the cases where the tab & tablist roles are not supported by the user’s browser & AT combo.

It will make most sense if we show you the new HTML first this time, rather than the CSS, and hopefully, without the cognitive clutter of the list and link tags, our end goal is now a little clearer. You will quickly see that the core DOM structure remains almost identical:

<div class="tabs">
  <div role="tablist">
    <div role="tab" aria-selected="true" tabindex="0">
      <span>Sign in</span>
    <div role="tab" aria-selected="false" tabindex="-1">
    <div role="tabpanel" id="sign-in" aria-hidden="false">
      <!-- Sign in Content -->
    <div role="tabpanel" id="register" aria-hidden="true">
      <!-- Register Content -->

With these new ARIA roles in place, our tabs will now actually be described as tabs by assistive technology. Likewise, when our JavaScript toggles the ARIA selected state, this state will also be conveyed to our users.

Note that AT actually requires two additional ARIA properties that are not present in our markup: aria-controls (on the tabs) and aria-labelledby (on the tabpanels). These ARIA properties are not typically used as styling hooks on tabs, so we will leave them out for the sake of code brevity; but be sure to include them when building your own tabs widget!

Okay, so we are nearing the end now, but first we must finish up our CSS. Our selectors must become a contract for the accessible HTML above. Where before we had classes for BEM blocks and elements, now we have ARIA roles. Where before we had classes for BEM modifiers, now we have ARIA states:

.tabs [role=tab][aria-selected=false][tabindex="-1"] {
  text-decoration: none;
.tabs [role=tab][aria-selected=true][tabindex="0"] {
  text-decoration: underline;
.tabs [role=tabpanel][aria-hidden=true] {
  display: none;
.tabs [role=tabpanel][aria-hidden=false] {
  display: block;

Personally, I’m a big fan of BEM, but it’s nice where possible like this to be able to replace it with something a little more real, if you know what I mean.

Finally, let us compare the accessibility tree of the first real tab with the first faux tab
Real tab Faux tab
Annotated accessibility tree of real tabs Annotated accessibility tree of faux tabs

One other rule we have enforced in our selectors is the tabindex attribute. Keyboard accessibility for tabs must be implemented in JavaScript using a roving tabindex technique; this is because the tabs in a tablist are selected using the arrow keys, not the tab key (the tab key is actually used to exit the list of tabs). While not strictly necessary to ensure the correct description is given, this selector helps ensure that the correct attribute values are in place for roving tabindex behavior. It’s up to you whether you want to go this far, into the realm of behavior-testing, in your own selectors.

Good behavior

We must always remember that correctly describing a UI control is only part of making it accessible. The user expectation is that it behaves like that control too. Therefore we must also ensure that the correct accessible behavior is in place.

For example, a button must always be ‘clickable’ with SPACE and ENTER keys. Sadly, this kind of behavior is often the first thing to go missing when developers try rolling their own buttons using span or div tags.

More complex controls such as tabs, menus, or autocomplete will typically require a more significant amount of JavaScript in order to make sure the control fully behaves according to its description.


We have seen that each layer of the web frontend has its own responsibilities in terms of creating accessible UI controls:

  • HTML provides the aural description and some built-in behavior
  • CSS provides the visual style and interaction clues
  • JS provides any missing behavior not provided by ARIA or HTML

HTML provides behavior, without the need for JavaScript, for built-in tags such as links, buttons, and form controls.

For the purpose of this blog post, our focus has been primarily HTML and CSS. HTML is fundamental in laying solid foundations for accessible UI controls and widgets, and we have shown how those foundations can be enforced by use of CSS attribute selectors.

So, the next time you find yourself creating a class name like ‘active’, ‘hidden’, ‘on’, or ‘off’ – stop and think instead how you might be able to leverage HTML properties or ARIA states in your selectors. Likewise, if you find yourself creating a class name like ‘btn’, ‘tab’, or ‘dialog’ – also stop and think about how you might be able to leverage an existing HTML tag or ARIA role.

Thank you for reading. I hope you enjoyed it. If you are interested in more accessibility-related articles in future, be sure to leave a comment below!

Finally, if you are interested in learning more about our CSS framework, watch this space for an upcoming announcement and further details. We are currently applying the finishing touches to the framework before releasing it as open source.

Appendix / bibliography