DEV Community

Cover image for Build an Accessible React Component: Part 1 - Breadcrumbs
Ashlee (she/her)
Ashlee (she/her)

Posted on • Edited on • Originally published at ashleemboyer.com

Build an Accessible React Component: Part 1 - Breadcrumbs

This was originally posted on my personal blog site.


This past Tuesday, the first stream in a series for building accessible React components, 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:
import React from "react";

const App = () => <div>Hello, world!</div>;

export default App;
Enter fullscreen mode Exit fullscreen mode
  • Rename src/index.css to index.scss
  • Update the reference to the file from 4 in src/index.js
  • Remove src/logo.svg
  • Run the app (yarn start or npm 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 or npm install node-sass in the terminal you've been working in so far
  • Re-run your app (yarn start or npm start)

Your browser should say "Hello, world!" now. We're all set up!

My process

  1. Read through the WAI-ARIA Authoring Practices documentation
  2. Create a minimal React component that says "Hello"
  3. Flesh out the React component with the necessary HTML elements
  4. Figure out what inputs (props) the React component needs
  5. Add the props to the component
  6. Add the necessary WAI-ARIA Roles, States, and Properties
  7. Add keyboard interaction
  8. Perform manual tests (listen with a screen reader, navigate with a keyboard, etc.)
  9. Add automated tests
  10. Write the documentation

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.

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:

src/
  components/
    Button/
    Dialog/
    Listbox/
    ...
  App.js
Enter fullscreen mode Exit fullscreen mode

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:

import React from "react";

const Breadcrumb = () => <h1>Breadcrumb works!</h1>;

export default Breadcrumb;
Enter fullscreen mode Exit fullscreen mode

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:

export { default as Breadcrumb } from "./Breadcrumb";
Enter fullscreen mode Exit fullscreen mode

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:

export { Breadcrumb } from "./Breadcrumb";
Enter fullscreen mode Exit fullscreen mode

Then update src/App.js to use the component:

import React from "react";

import { Breadcrumb } from "./components";

const App = () => <Breadcrumb />;

export default App;
Enter fullscreen mode Exit fullscreen mode

Check your browser. It should say "Breadcrumb works!" with an <h1> element.

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.

<nav>
  <ol>
    <li>
      <a href="">Link 1</a>
    </li>
    <li>
      <a href="">Link 2</a>
    </li>
    <li>
      <a href="">Link 3</a>
    </li>
  </ol>
</nav>
Enter fullscreen mode Exit fullscreen mode

Save your component and you should see something like the following in your browser:

1. Link 1
2. Link 2
3. Link 3
Enter fullscreen mode Exit fullscreen mode

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:
import styles from "./Breadcrumb.module.scss";
Enter fullscreen mode Exit fullscreen mode
  • Give the nav element in the component a className:
<nav className={styles.BreadcrumbContainer}>...</nav>
Enter fullscreen mode Exit fullscreen mode
  • Add the code to Breadcrumb.module.scss:
.BreadcrumbContainer {
  padding: 12px;
  background-color: lightgray;
  text-align: left;

  ol {
    margin: 0;
    padding: 0;
    list-style: none;

    li {
      display: inline;
      margin: 0;
      padding: 0;

      a {
        color: black;
      }
    }
  }

  // The visual separators
  li + li::before {
    display: inline-block;
    margin: 0 12px;
    transform: rotate(15deg);
    border-right: 2px solid black;
    height: 0.8em;
    content: "";
  }
}
Enter fullscreen mode Exit fullscreen mode

The links should be listed horizontally on a gray background have separators between each.

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:

<Breadcrumb
  links={[
    {
      label: "Link 1",
      href: "",
    },
    {
      label: "Link 2",
      href: "",
    },
    {
      label: "Link 3",
      href: "",
    },
  ]}
/>
Enter fullscreen mode Exit fullscreen mode

Now we need to update the component to accept and use a prop called links.

const Breadcrumb = ({ links }) => (
  <nav className={styles.BreadcrumbContainer}>
    <ol>
      {links.map(link => (
        <li>
          <a href={link.href}>{link.label}</a>
        </li>
      ))}
    </ol>
  </nav>
);
Enter fullscreen mode Exit fullscreen mode

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.

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:

<nav aria-label="Breadcrumb">...</nav>
Enter fullscreen mode Exit fullscreen mode

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. Here's what the <ol> element should look like now:

<ol>
  {links.map((link, index) => {
    const isLastLink = index === links.length - 1;
    return (
      <li>
        <a href={link.href} aria-current={isLastLink ? "page" : undefined}>
          {link.label}
        </a>
      </li>
    );
  })}
</ol>
Enter fullscreen mode Exit fullscreen mode

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:

[aria-current="page"] {
  font-weight: bold;
  text-decoration: none;
}
Enter fullscreen mode Exit fullscreen mode

You can read more about the aria-current state here.

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.

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:

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.

  1. Run npm i --save-dev enzyme enzyme-adapter-react-16 in your terminal to install Enzyme with npm

  2. Add the following code to the end of the setupTests.js file to configure Enyzme:

import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

configure({ adapter: new Adapter() });
Enter fullscreen mode Exit fullscreen mode

Writing the tests

Since the file is short, I'll paste it now and then walk through what's going on.

import React from "react";
import { shallow } from "enzyme";

import Breadcrumb from "./Breadcrumb";

const testLinks = [
  { label: "Test Link 1", href: "test-link-1" },
  { label: "Test Link 2", href: "test-link-2" },
];

describe("<Breadcrumb />", () => {
  it("renders successfully with the correct aria attributes", () => {
    const wrapper = shallow(<Breadcrumb links={testLinks} />);

    const nav = wrapper.find("nav");
    expect(nav).toHaveLength(1);
    expect(nav.props()["aria-label"]).toBe("Breadcrumb");

    const anchorElements = wrapper.find("a");
    expect(anchorElements).toHaveLength(testLinks.length);

    const firstAnchor = anchorElements.first();
    expect(firstAnchor.text()).toBe(testLinks[0].label);
    expect(firstAnchor.props()["href"]).toBe(testLinks[0].href);

    const lastAnchor = anchorElements.last();
    expect(lastAnchor.props()["aria-current"]).toBe("page");
  });
});
Enter fullscreen mode Exit fullscreen mode

After all the necessary imports, we have a links 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 expects as we want. We've got quite a few here, so let's see what each one testing.

  1. 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.

  2. One of our specifications for the component is that it wraps everything in a <nav> element and that the element has aria-label="Breadcrumb" on it. We test for that by using find. We only want there to be 1 element, so that's what the first expect is accomplishing. Then, we want to check the props on the nav and make sure the aria-label prop is correctly set to "Breadcrumb".

  3. 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, we find all the <a> elements and then expect that there are as many found as we have in our testLinks array.

  4. Now we can look at the first link rendered to make sure it has both a label and href being rendered correctly. We get the first anchor element using the handy first method. Then we expect it's text to match the first test link's label. Finally, we check the props on the element and make sure href is set to the test link's href. 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.

  5. 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 a last method to go with first. Similar to how we checked the aria-label prop in 2, we expect it to have the string value of "page".

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.
# Breadcrumb

This component displays a list of links to show users where they are within an application.
Enter fullscreen mode Exit fullscreen mode
  • 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.
## Properties

**Links**

- Type: Array
- Required: Yes
- Default value: None
- Description: These are the links to show in the breadcrumb. Each has a `label` and an `href` attribute.
Enter fullscreen mode Exit fullscreen mode
  • 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.
## Accessibility

### Keyboard Interaction

Not applicable.

### WAI-ARIA Roles, States, and Properties

- The links are contained in an ordered list within a `<nav>` element
- The `<nav>` element has the `aria-label` attribute set to `"Breadcrumb"`
- The last link in the list represents the current page, and must have `aria-current` set to `"page"`

### Additional Features

- The separators between each link are added via CSS so they are not presented by a screen reader
Enter fullscreen mode Exit fullscreen mode
  • 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.
## Usage

<Breadcrumb
  links={[
    { label: "Link 1", href: "" },
    { label: "Link 2", href: "" },
    { label: "Link 3", href: "" }
  ]}
/>
Enter fullscreen mode Exit fullscreen mode

Conclusion

And that's it! We have an accessible Breadcrumb component. Join me on Twitch every Tuesday at 7pm EDT for live coding! 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!


Did you know I have a newsletter? 📬

If you want to get notified when I publish new blog posts or make major project announcements, head over to https://ashleemboyer.com/newsletter.

Top comments (0)