Build an Accessible Breadcrumb Component -- Part 1 in the Accessible React Component Series
Published Saturday, April 25, 2020 — 14 minute read
During the first stream in the accessible React component live coding series, we spun the wheel and it chose the breadcrumb component for us! While the component was extremely straightforward, I think it was a great one to kick off the series. Let's dive right in, shall we?
Setup
You can skip this part if you already have your own React project set up. This section is for anyone who wants to follow the series with a fresh project.
- Run
npx create-react-app <project-name>
in your terminal - Remove the
src/App.css
file - Replace your
src/App.js
file with this code:
1import React from 'react';
2
3const App = () => <div>Hello, world!</div>;
4
5export default App;
6
- Rename
src/index.css
toindex.scss
- Update the reference to the file from 4 in
src/index.js
- Remove
src/logo.svg
- Run the app (
yarn start
ornpm start
)
Now, you should see a "Failed to compile" error in your browser and it should be because we haven't added the node-sass
package to or project yet.
- Run
yarn add node-sass
ornpm install node-sass
in the terminal you've been working in so far - Re-run your app (
yarn start
ornpm start
)
Your browser should say "Hello, world!" now. We're all set up!
My process
- Read through the WAI-ARIA Authoring Practices documentation
- Create a minimal React component that says "Hello"
- Flesh out the React component with the necessary HTML elements
- Figure out what inputs (props) the React component needs
- Add the necessary WAI-ARIA Roles, States, and Properties
- Add keyboard interaction
- Perform manual tests (listen with a screen reader, navigate with a keyboard, etc.)
- Add automated tests
- Write the documentation
1. The WAI-ARIA Authoring Practices Documentation
The first thing we have to do is read the available documentation for this component on the WAI-ARIA Authoring Practices web page. There's not too much to this component.
A breadcrumb trail consists of a list of links to the parent pages of the current page in hierarchical order. It helps users find their place within a website or web application. Breadcrumbs are often placed horizontally before a page's main content.
There's no keyboard interaction to add here since you can use the Tab and Shift+Tab keys by default to navigation through links. We just have to make sure we're using the correct HTML elements in our component and we have one ARIA state (aria-current
) and one ARIA property (aria-label
) to include as well.
2. A Minimal React Component
This series of blog posts will use the file structure I have laid out in my a11y-components
GitLab repository. It looks a bit like this:
1src/
2 components/
3 Button/
4 Dialog/
5 Listbox/
6 ...
7 App.js
8
Let's add a Breadcrumb
folder under components
. You need to create the components
folder and add an index.js
file to it if you followed the Setup section above. Then we need to add 5 files to the Breadcrumb folder:
- Breadcrumb.jsx
- Breadcrumb.module.scss
- Breadcrumb.test.js
- index.js
- README.md
Breadcrumb.jsx
This file will have all of our React code. Let's start with something minimal to check if our setup is correct:
1import React from 'react';
2
3const Breadcrumb = () => <h1>Breadcrumb works!</h1>;
4
5export default Breadcrumb;
6
Breadcrumb.module.scss
This file will hold all of our CSS. We'll wait to add anything here until we start building up the component.
Breadcrumb.test.js
Don't forget to write tests! They're not only important for making sure your component works as expected, but also for making sure that future changes you make don't break existing behavior. We'll write these after we've finished the component.
index.js
This file is for exporting everything we need from the Breadcrumb component so it can be used elsewhere in the application. More complex components might have multiple exports in this file but ours will stay simple for this component:
1export { default as Breadcrumb } from './Breadcrumb';
2
README.md
This is where we'll document our component. It's important to detail a component's purpose and how to use it. We'll have 3 main sections: Properties, Accessibility, and Usage (examples). Let's also save this file for when the component is done.
Test it out
First add the following to the src/components/index.js
file:
1export { Breadcrumb } from './Breadcrumb';
2
Then update src/App.js
to use the component:
1import React from 'react';
2
3import { Breadcrumb } from './components';
4
5const App = () => <Breadcrumb />;
6
7export default App;
8
Check your browser. It should say "Breadcrumb works!" with an <h1>
element.
3. Add HTML elements to the React Component
Now that our component has all its files created and we've got a minimal version of it working and showing in our browser, we can start building it out to specification. Let's head back over to the documentation and see what elements we need to use. You should see an "Example" section for the widget and a single link to the example. Let's go there.
Under "Accessibility Features" we can see that we need a <nav>
element to contain all of the links and that the links need to be structured in an ordered list (<ol>
) component. Don't worry about how the elements need to be labeled just yet. We'll get to that in a few minutes.
Let's change what our Breadcrumb component renders first. We can hardcode the elements for now and then make the component more dynamic in the next step.
1<nav>
2 <ol>
3 <li>
4 <a href="">Link 1</a>
5 </li>
6 <li>
7 <a href="">Link 2</a>
8 </li>
9 <li>
10 <a href="">Link 3</a>
11 </li>
12 </ol>
13</nav>
14
Save your component and you should see something like the following in your browser:
11. Link 1
22. Link 2
33. Link 3
4
Yay! Now we need to style the list horizontally and add a separator between each link. We're going to do this in CSS so that screen readers don't pick them up and present them to users.
- Import the SCSS file in
Breadcrumb.jsx
:
1import styles from './Breadcrumb.module.scss';
2
- Give the
nav
element in the component aclassName
:
1<nav className={styles.BreadcrumbContainer}>...</nav>
2
- Add the code to
Breadcrumb.module.scss
:
1.BreadcrumbContainer {
2 padding: 12px;
3 background-color: lightgray;
4 text-align: left;
5
6 ol {
7 margin: 0;
8 padding: 0;
9 list-style: none;
10
11 li {
12 display: inline;
13 margin: 0;
14 padding: 0;
15
16 a {
17 color: black;
18 }
19 }
20 }
21
22 // The visual separators
23 li + li::before {
24 display: inline-block;
25 margin: 0 12px;
26 transform: rotate(15deg);
27 border-right: 2px solid black;
28 height: 0.8em;
29 content: '';
30 }
31}
32
The links should be listed horizontally on a gray background have separators between each.
4. Add Props to the React Component
Let's make our component accept a list of links so it's dynamic and can be reused. It looks like each link has two pieces: a readable label and an href
. We first need to update src/App.js
and pass an array of links to the component like this:
1<Breadcrumb
2 links={[
3 {
4 label: 'Link 1',
5 href: '',
6 },
7 {
8 label: 'Link 2',
9 href: '',
10 },
11 {
12 label: 'Link 3',
13 href: '',
14 },
15 ]}
16/>
17
Now we need to update the component to accept and use a prop called links
.
1const Breadcrumb = ({ links }) => (
2 <nav className={styles.BreadcrumbContainer}>
3 <ol>
4 {links.map((link) => (
5 <li>
6 <a href={link.href}>{link.label}</a>
7 </li>
8 ))}
9 </ol>
10 </nav>
11);
12
When you look at the browser, it should look exactly as it did before this step if you're using the same links as you previously hardcoded.
5. WAI-ARIA Roles, States, and Properties
We have two ARIA attributes to discuss for this component: aria-label
and aria-current
.
aria-label
This attribute describes the kind of navigation the component is providing. It must be set to "Breadcrumb" like this:
1<nav aria-label="Breadcrumb">...</nav>
2
You can read more about the aria-label
property here.
aria-current
This attribute is applied to the last link in the list so it will be presented as the current page's link. We can accompomplish this by using the second parameter passed to our callback to the map
method, which is the index of the current element in the array. If the index we're looking at is one less than the length of the index, then we're looking at the last element in the array and need to apply the aria-current="page"
attribute to the <a>
element we're rendering. Otherwise, the attribute should be undefined
. Let's also add a unique key
to each <li>
while we're at it.
Here's what the <ol>
element should look like now:
1<ol>
2 {links.map((link, index) => {
3 const isLastLink = index === links.length - 1;
4 return (
5 <li key={`breadcrumb-link-${index}`}>
6 <a href={link.href} aria-current={isLastLink ? 'page' : undefined}>
7 {link.label}
8 </a>
9 </li>
10 );
11 })}
12</ol>
13
We also probably want to style the current page's link differently to indicate that it's the page we're on. We can do this in our SCSS file by selecting on the aria-current
attribute. You'll want to add this to the ol
section of the file:
1[aria-current='page'] {
2 font-weight: bold;
3 text-decoration: none;
4}
5
You can read more about the aria-current
state here.
6. Add Keyboard Interaction
We don't have any keyboard interaction to add to this component! We just need to make sure that Tab and Tab+Shift work as expected with <a>
elements.
7. Perform Manual Tests
I use the ChromeVox Classic Extension to to do screen reader testing. It's easy to turn on only when I want to do tests by going to chrome://extensions/
in my browser and toggling the extension on and off.
Here's a video of what the component looks and sounds like when you tab through it:
8. Add Automated Tests
The tests for this component should be very straightforward since there's no interaction or state changes going on. We don't need to test what happens on click and there's no computation or anything of like going on. This component just loads and shows things, that means the only thing we can really test for is that everything shows correctly on load. We'll be using Jest and Enzyme for testing.
Setting up Enzyme
First, we need to install and configure Enzyme. You can skip to the next section if you've already got it working.
Run
npm i --save-dev enzyme enzyme-adapter-react-16
in your terminal to install Enzyme with npmAdd the following code to the end of the
setupTests.js
file to configure Enyzme:
1import { configure } from 'enzyme';
2import Adapter from 'enzyme-adapter-react-16';
3
4configure({ adapter: new Adapter() });
5
Writing the tests
Since the file is short, I'll paste it now and then walk through what's going on.
1import React from 'react';
2import { shallow } from 'enzyme';
3
4import Breadcrumb from './Breadcrumb';
5
6const testLinks = [
7 { label: 'Test Link 1', href: 'test-link-1' },
8 { label: 'Test Link 2', href: 'test-link-2' },
9];
10
11describe('<Breadcrumb />', () => {
12 it('renders successfully with the correct aria attributes', () => {
13 const wrapper = shallow(<Breadcrumb links={testLinks} />);
14
15 const nav = wrapper.find('nav');
16 expect(nav).toHaveLength(1);
17 expect(nav.props()['aria-label']).toBe('Breadcrumb');
18
19 const anchorElements = wrapper.find('a');
20 expect(anchorElements).toHaveLength(testLinks.length);
21
22 const firstAnchor = anchorElements.first();
23 expect(firstAnchor.text()).toBe(testLinks[0].label);
24 expect(firstAnchor.props()['href']).toBe(testLinks[0].href);
25
26 const lastAnchor = anchorElements.last();
27 expect(lastAnchor.props()['aria-current']).toBe('page');
28 });
29});
30
After all the necessary imports, we have a testLinks
constant that holds the test values we need to perform our tests. It's good practice to store test values rather than hardcoding them inline for the same reason we don't want to do it in other code: it makes it easier to modify the test values. It's not fun trying to update a bunch of strings in a test file with a few hundred lines of code. Variables are so easy to reference in tests!
Then, we have our main describe
block that groups all of the tests for this component. We have a single it
block (alias for test
) which runs our single test. In our test, we can then call as many expect
s as we want. We've got quite a few here, so let's see what each one testing.
First, we shallow render the component. This is an Enzyme concept and you can read about it and it's API Reference at this link.
One of our specifications for the component is that it wraps everything in a
<nav>
element and that the element hasaria-label="Breadcrumb"
on it. We test for that by usingfind
. We only want there to be 1 element, so that's what the first expect is accomplishing. Then, we want to check theprops
on thenav
and make sure thearia-label
prop is correctly set to"Breadcrumb"
.Next, we want to make sure the correct number of anchor elements are being rendered based on the input given to the component through the
links
prop. Similar to the previous step, wefind
all the<a>
elements and then expect that there are as many found as we have in ourtestLinks
array.Now we can look at the first link rendered to make sure it has both a
label
andhref
being rendered correctly. We get the first anchor element using the handyfirst
method. Then we expect it'stext
to match the first test link'slabel
. Finally, we check theprops
on the element and make surehref
is set to the test link'shref
.
Note: we only need to perform these expects on the first element because if the first element is rendered correctly, then all the others are too.
- Last but not least, we need to make sure the last anchor element has the
aria-current
attribute set to"page"
. And you guessed it! Enzyme also has alast
method to go withfirst
. Similar to how we checked thearia-label
prop in 2, we expect it to have the string value of"page"
.
9. Write the Documentation
We're almost done! Let's get the documentation written out and then we can admire our beautiful new component as a whole.
- Open up the Breadcrumb's
README.md
and add an H1 heading and a description/purpose of the component.
1# Breadcrumb
2
3This component displays a list of links to show users where they are within an application.
4
- Add an H2 heading for Properties. This is where we'll describe the props passed into the component. This should be in a table in your file, but for formatting purposes, I am listing them below as a list.
1## Properties
2
3**Links**
4
5- Type: Array
6- Required: Yes
7- Default value: None
8- Description: These are the links to show in the breadcrumb. Each has a `label` and an `href` attribute.
9
- Add another H2 heading for Accessibility. We'll detail keyboard interaction, WAI-ARIA roles, states, and properties, and additional features, just like the WAI-ARIA site does.
1## Accessibility
2
3### Keyboard Interaction
4
5Not applicable.
6
7### WAI-ARIA Roles, States, and Properties
8
9- The links are contained in an ordered list within a `<nav>` element
10- The `<nav>` element has the `aria-label` attribute set to `"Breadcrumb"`
11- The last link in the list represents the current page, and must have `aria-current` set to `"page"`
12
13### Additional Features
14
15- The separators between each link are added via CSS so they are not presented by a screen reader
16
- Last but not least, we add an H2 heading for Usage. This is where we'll put some code examples for how to use the component.
1## Usage
2
3<Breadcrumb
4links={[
5{ label: "Link 1", href: "" },
6{ label: "Link 2", href: "" },
7{ label: "Link 3", href: "" }
8]}
9/>
10
Conclusion
And that's it! We have an accessible Breadcrumb component. We still have many more accessible React components to make and it's been so much fun this far. Make sure to follow my channel so you're notified every time I go live!