Templating Accessible Components in React Part 3: Buttons

In the first part of this series, we introduced the idea of building reusable accessible components in React to lower the effort needed to build an accessible application, and covered how to build Navigation components; in Part 2 we focused on accessible inputs.


Another commonly used component for any application is a button. A button requires an onClick action; only use a button when the user is required to click. A link on the other hand should be used for navigation. You can style a link to look like a button and vice versa but never use them interchangeably.

You may use a div= or a and give it the role of button. The downside with a div or a is that you can not use the spacebar to trigger the onClick behavior. A button however will call the onClick with a click, enter key, or spacebar, making it a more accessible option.

You can add an aria-describedby to the button as well to describe the button to users. For instance, take the earlier implementation of the button. We created a cancel button but it would be nice to know what you are cancelling. So, you can add a div beneath the button with an id that corresponds to the aria-describedby. If you don’t want that displaying on the page, you can give it a className of sr-only (if you are using Bootstrap). This hides the element from the page but keeps it readable by screen readers.

To ensure this behavior across our application let us create a custom button component.

Custom Button Component

import React from 'react';
const { isEmpty } from 'lodash'
interface IProps {
action: () => void
description: string
role?: string
text: string
const CustomButton: React.FC<IProps> = ({ description, role, text, action }) => {
const isButton: boolean = !role
const doesTextExist: boolean = !isEmpty(text) || text !== ' '
const doesDescriptionExist: boolean = !isEmpty(description) || description !== ' '
const renderAccessibilityError = (): JSX.Element => {
  const errorMessage: string | null = !doesTextExist
    ? 'text for your button'
    : !doesDescriptionExist
    ? 'a description for your button'
    : null
  return <p className='text-danger'>Please provide {errorMessage} </p>
const renderSpan = (): JSX.Element => <span role={role} aria-describedby={description}>{text}</span>
const renderButton = (): JSX.Element => <button aria-describedby={description} onClick={action}>{text}</button>
const renderButtonOrSpanNode = (): JSX.Element => isButton ? renderButton() : renderSpan()
return (
  { (doesTextExist || doesDescriptionExist) ? renderAccessibilityError() : renderButtonOrSpanNode() }

Much of what is going on here is akin to the NavItem and AccessibleInput components. We take in a few props that we know we need to make the button accessible: a description for the aria-describedby attribute; a role for any “button” that is an HTML tag other than a button; button text; and, an action for what happens when the user clicks the button.

We then want to create an error method if the developer forgets to add a description or text. The renderAccessibilityError is pretty much identical to the error method defined in the NavItem component. The renderSpan component returns a span tag with a role and the text. We want to return a span (or any other HTML tag you want) only if there is a role provided. That tells us that the developer does not intend to use a native <button> tag  but does wish to use something like a button.

You can also accept a tag prop that determines what HTML tag to use instead of defaulting to a span tag.

While this is not ideal, we can still allow it as long as there is a role attached.  The renderButton method returns an HTML button with an aria-describedby attribute that equals the description passed down.  Finally, we have the renderButtonOrSpanNode that determines whether we should render an HTML button or a span tag. The const isButton determines if there is a role present, if it is than we are not supposed to render a native <button>.

Using the Custom Button Component

To use this button, we want to pass down all of the required props. Going back to our form component, we can add an onSubmit button.

import AccessibleInput from './AccessibleInput'
import CustomButton from './CustomButton'
const AccessibleForm: React.FC = () => {
 const [apiRequest, setApiRequest] = useState<object>({})
 const handleOnBlur = (requestAttribute: string) => (
   e: React.ChangeEvent<HTMLInputElement>
 ) =>
   setApiRequest({ ...apiRequest, [requestAttribute]: })
// + Added
const handleOnSubmit = (): void => {
 // do something
 return (
       label='First Name'
   {/* + Added */}
   <CustomButton text='Submit' description='Click this to submit the signup form' action={handleOnSubmit} />
export default AccessibleForm

The handleOnSubmit action will be invoked whenever the user clicks on the button. Since we have passed both text and description our button will display otherwise we will see an error message.

Now you have an accessible button component. You can turn this into a custom hook as well or spruce it up with classes or more props.

Checking Accessibility

There are many tools to check if your application is accessible. You can use Lighthouse in your dev tools on your browser to run an audit and find different areas of the page that are failing accessibility requirements. You can also download browser plugins to check the accessibility of the page (some are more detailed than others). Also, there are many React packages that provide accessible components if you prefer not to build your own.

The takeaway here is that accessibility does not have to increase your application cost. You do not need to refactor your entire application to make it accessible. You can start small and refactor simple but widely used components into accessible components, then reuse those accessible components in all of your applications. Take it a component at a time. Every chance you get, turn a component into an accessible component, and you’ll be on your way to building accessible applications.

Read the other posts in our Templating Accessible Components series: Part 1: Navigation; Part 2: Inputs

Header photo by Arisa Chattasa on Unsplash

You've successfully subscribed to SmartLogic Blog
Great! Next, complete checkout for full access to SmartLogic Blog
Welcome back! You've successfully signed in.
Unable to sign you in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.