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.
Buttons
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 atag
prop that determines what HTML tag to use instead of defaulting to aspan
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]: e.target.value })
// + Added
const handleOnSubmit = (): void => {
// do something
}
return (
<form>
<AccessibleInput
label='First Name'
requestAttribute='first_name'
type='text'
handleOnBlur={handleOnBlur}
/>
.
.
.
{/* + Added */}
<CustomButton text='Submit' description='Click this to submit the signup form' action={handleOnSubmit} />
</form>
)
}
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