Ingredients for Better Accessibility — Part One: The Basics

Mike Polowick
Jan 12 · 8 min read
Spoonfuls of exotic spices including paprika
Spoonfuls of exotic spices including paprika
Photo by Pratiksha Mohanty on Unsplash

At Galvanize we have built a UI library of React components (Paprika) to implement our design system (Starling). One of the core principles of this library is to be usable — which implies usable for everyone. To fulfill this vision we have committed to making our components as accessible as possible.

As much as possible, we will bake the accessibility implementation right into the component so that anyone consuming that component will not need to know the details. This is not always possible, so in many cases we strive to provide components that enable and facilitate the development of accessible web applications.

In this series I’d like to take a closer look at some of the specific choices we’ve made in our component library to achieve this goal. We are proud of the work we’ve done to create a UI library with a spirit of inclusiveness and hope that sharing these details can advance the state of accessibility in the web development field.

Fake Buttons

It is universally recommended by accessibility experts to use a semantic <button> HTML element any time you need to use a button on a web page. Simply adding onClick to a plain old <div> is a terrible idea!

  • It will not be recognized as a button by assistive technology, like screen readers (WCAG 1.3.6, 4.1.2).
  • It will not receive keyboard focus when the user “tabs” through the page (WCAG 2.1.1, 2.4.7).
  • It cannot be activated when the user presses enter or space on the keyboard (WCAG 2.1.1).

Using a native <button> will work better and definitely be more accessible than a fake <div onClick={handleClick}> button.

But a <button> element looks like a button. What if you want something to be clickable, but you don’t want it to look like a button? Custom UI widgets may well need clickable elements that do not look like standard browser buttons.

What if you are maintaining an existing web site or building new features for an old web application that includes global CSS targeting the <button> element? Then you will spend a lot of time and effort fighting with those CSS rules. Ideally you would remove the offending global CSS, but that is an unrealistically large amount of work and testing in many cases. For a UI component used in multiple legacy web applications, this is not a realistic solution.

That is why there is a <RawButton> component in Paprika.

The <RawButton> disregards the conventional wisdom of using a <button> and renders a raw, unstyled <span> with all the functionality and accessibility implemented from scratch.

In addition, the Paprika <Button> component includes an isSemantic prop that will allow styled button components to be rendered as either a native <button> element or a raw, accessible <span> that is resilient to global CSS. For new applications, we still prefer to leave isSemantic=true (the default) but using isSemantic=false has the same visual and functional result.

Screenshot of a semantic button and a raw button side-by-side with source code showing the underlying HTML markup.
Screenshot of a semantic button and a raw button side-by-side with source code showing the underlying HTML markup.
The <Button isSemantic={false}> is simply a <RawButton> with button styling. For a user, there is no difference.

As a bonus, since we are using styled-components to implement the <RawButton>, and because we relay any additional props/attributes passed to the component down to the underlying root node, it’s trivial to render the <RawButton> as any kind of element. For example, if a table row element needs to be clickable, it may be necessary to apply click handlers directly to the <tr> because a <span> cannot fulfill the role of a table row. With Paprika we can simply do this:

<RawButton as=”td” onClick={handleClick}>Vegan gastropub</RawButton>

As simple as the <RawButton> component is, it has been an extraordinarily versatile and effective tool in building accessible compound components and features that are resilient to global styling in legacy applications.


Formatting a web page with semantic headings is important for accessibility compliance (WCAG 1.3.1, 2.4.6, 2.4.10) but these success criteria only require that headings are used, not that they conform to a proper hierarchy. Maintaining a proper heading structure on the page that does not skip heading levels is a highly encouraged best practice, and will be flagged by accessibility testing tools like Deque’s axe.

There is a good reason for this too — users who depend on screen readers make extensive use of headings to scan a page quickly and find the information of interest. In a survey by WebAIM, over 86% of users responded that heading levels are useful for navigating a web page. By far the most common strategy users employ when trying to find information on a lengthy page is to navigate by headings.

The visual design of a page does not always conform rigidly to this hierarchy — a heading that should be level two (<h2>) may be styled to look smaller and more subtle, like what a level five heading would look like. It’s fine for a heading to look smaller, as long as the heading actually is an <h2> element in the markup. This can be accomplished easily enough with CSS, but to alleviate any temptation for developers to simply use an <h5> element instead, we have baked this capability into the API for the Paprika <Heading> component with the a level and a displayLevel prop. This makes it simple to produce an <h2> that looks like an <h5>:

<Heading level={2} displayLevel={5}>Cold-pressed Hexagon</Heading>
Screenshot of Paprika level 2 heading styled like a level 5 heading with document source showing HTML h2 element.
Screenshot of Paprika level 2 heading styled like a level 5 heading with document source showing HTML h2 element.
A Paprika <Heading> decouples the styling from the markup so the the proper document hierarchy can be maintained without compromising the design.

Invisible Headings

There are even some cases where the visual layout bestows enough context that a heading is not necessary and in fact would clutter the design. But for a screen reader user, it is difficult to understand the context or to navigate quickly to the related content without the heading landmark. To improve the experience for users relying on a screen reader, a simple solution is to include a visually hidden heading on the page. Again, this is simple with the Paprika component:

<Heading level={2} isHidden>Gastropub skateboard</Heading>

Similarly, a very brief heading may be appropriate in the context of a visual layout, but without cues such as the proximity to related items, a heading that is too short may be ambiguous for screen reader users. In this case it’s possible to override the visual content of the <Heading> with an aria-label attribute that takes precedence for assistive technology. We insulate consumers of this component (and many other Paprika components) from the implementation details of ARIA by providing a generic a11yText prop for this kind of purpose:

<Heading level={3} a11yText=”Asymmetrical lo-fi distillery vaporware”>Asymmetrical</Heading>

These features make the <Heading> an effective tool for dramatically improving the experience for users relying on assistive technology.

Alert Messages

Pages in modern web applications are not static — there is a lot of dynamic content that changes without a page refresh. For sighted users, these changes to dynamic “live regions” on a page are obvious, but for users that rely on screen readers or other assistive technology, these changes are imperceptible unless handled deliberately.

An alert message is a typical example of this kind of dynamic content. When an asynchronous task begins, completes successfully, or terminates with an error, a notification is displayed, inline or overlaid at the top of the viewport, with the details of the event. In Paprika, there is a <Toast> component for this purpose.

A green notification overlay that reads “your data was saved successfully”.
A green notification overlay that reads “your data was saved successfully”.
A Paprika <Toast> notifies the user of a successful operation.

The <Toast> component is rendered as an ARIA live region by setting the role attribute on its root node to status or alert. The <Toast> has a prop called isPolite which is set to true by default, meaning it has the role=status attribute applied and its content will be announced after any current announcement finishes. If the message is urgent, then the isPolite prop can be set to false, the role will be set to alert, and the message will be read immediately. Pretty straightforward.

The catch is that assistive technology needs to be aware of the live region before the content is rendered. Ideally all live regions exist when the page first loads — then they can be observed for any changes, which will be announced when they are detected. However, with a component-based page design, the components are often being added and removed from the DOM as needed, so it is unlikely and impractical to render the live region for a component before it’s needed. Interestingly, during our initial development we found that with VoiceOver in Chrome and Safari it worked fine to mount a live region to the page with content — it would still be announced to the user. This was not the case, however, with JAWS on Windows.

The solution that has allowed us to maintain the componentized nature of the <Toast> is to first mount and render an empty live region container, then after a very short delay (20ms) the content (children) is rendered. This was successful in VoiceOver, JAWS, and NVDA.

Invisible Toast

Frequently with dynamic web applications, a user event will invoke the need to fetch more data. When that data is received, the UI is updated in some way with the new details. For sighted users, this is a natural process, and a notification would be redundant. But for users who rely on a screen reader there will be no indication when, or even if, that data becomes available.

For example, in a table that has hundreds of rows, the user has the option to apply search filters, such as showing only rows that contain their username. Applying that filter will result in a new set of data being retrieved from the server, a momentary loading spinner, followed by a refreshed table with only those filtered rows. Since the keyboard focus will remain on the search filter widget during the fetching event, a screen reader user will not hear any of the UI changes.

The solution for this common use case is quite simple, render a <Toast> but don’t show it to sighted users. The <Toast> has a kind prop that controls how the notification looks — like a success message, error message, etc. One of the values it can have is visually-hidden. This will apply the common accessibility technique of positioning the element well outside the viewport with CSS (but importantly not by applying display:none or visibility:hidden).

Since this element does exist on the page somewhere, it’s possible for the user to navigate to it while in “reading” mode with their screen reader and discover the notification or alert message out of context. To prevent this, the canAutoClose prop on the <Toast> can be used to automatically remove the message from the DOM after a delay that will give the screen reader enough time to finish reading the announcement.

return shouldRenderNotification 
? <Toast
20 more results have been displayed.
: null;

This technique allows dynamic changes to be perceived in a non-visual way, with minimal development effort.


These basic components have made it far simpler for us to cook up more accessible features in the applications we make. They’ve also been key ingredients in many of the more complex Paprika components. All of these components are open source on GitHub and published on NPM so you are free to examine the implementation details or use them in your own project.

I have some more ideas marinating that I’d like to share about some of our more sophisticated components, like the <FormElement>, and the focus locking technique we implement for our modal components, but will save those for another article, so stay tuned. Until next time, happy coding!

Build Galvanize

A window to the product, design, and engineering teams at…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store