Accessibility (a11y) by default with React and Typescript

Accessibility (a11y) by default with React and Typescript

·

6 min read

Accessibility (a11y for short) is a major concern when creating modern websites. And for a very good reason. The internet is for everyone and we, as developers, need to make sure we build it that way!

When it comes to React, most developers are well aware of the benefits of reusable components - they provide consistency and flexibility while keeping our code clean.

But here's a little secret - React components, when built with a11y in mind, can also dramatically improve the level of accessibility of our UIs. What is more, by adding Typescript to the mix, we can practically enforce certain accessibility features directly in our codebase.

Let's explore some examples.

Images

Imagine we are building a reusable image component for our React project. In its simplest form, it might look something like this:

type ImageProps = {
  src: string;
};

const Image = ({ src }: ImageProps) => {
      return <img src={src} />;
};

But to make our component truly accessible, we need to create the possibility to add some alt text to it. The alt attribute could be an empty string for decorative images but it should always be there.

How should we go about it? The simplest way is to just add an optional alt prop like so:

type ImageProps = {
  src: string;
  alt?: string
};

const Image = ({ src, alt }: ImageProps) => {
     return <img src={src} alt={alt} />;
};

This works just fine! But it still doesn't ensure our component is accessible out of the box. Developers could still simply omit the alt prop when they are using the component. So let's make sure we always have it, even if it's an empty string by adding a default value.

type ImageProps = {
  src: string;
  alt?: string
};

const Image = ({ src, alt="" }: ImageProps) => {
    return  <img src={src} alt={alt} />;
};

This is better! We've made sure our component is accessible by default. But we can take it a step further still by making the alt prop mandatory:

type ImageProps = {
  src: string;
  alt: string
};

const Image = ({ src, alt }: ImageProps) => {
     return <img src={src} alt={alt} />;
};

Why do this when we already had an excellent solution? Simple. With this setup, we are forcing the developers on our team to actively consider whether the image they are adding to the code is decorative or not. Accessibility is now something about which we make conscious choices. It is no longer merely an afterthought.

Inputs and labels

Inputs are another fundamental React component we can easily make accessible by default. Let's create a basic Input component to illustrate:

type InputProps = {
  label?: string;
  ... // other relevant input props
};

const Input = ({ label }: InputProps) => {
  return (
    <>
      {label && <label>{label}</label>}
      <input type="text" />
    </>
  );
};

The component above is clearly not accessible. Not only is the label is not associated with the corresponding input but it will also not even render, if we fail to pass the label prop to our component. Let's fix it.

type InputProps = {
  id: string;
  label: string;
  ... // other relevant input props
};

const Input = ({ label, id }: InputProps) => {
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} type="text" />
    </>
  );
};

Much better! We now have a mandatory id and label props that ensure our component is accessible out of the box. But there is a problem. We always need to have a visible label for our inputs. This is not ideal because in some cases, we might want to hide it.

To implement the necessary hidden label functionality without breaking accessibility, we should have a mechanism to visually hide the label while still having it available in the DOM and appropriately styled.

Here's an example implementation:

// CSS for our visually-hidden class

.visually-hidden {
  border: 0;
  clip: rect(0 0 0 0);
  height: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
  width: 1px;
}

// React component

type InputProps = {
  id: string;
  label: string;
  labelIsVisuallyHidden?: boolean;
  ... // other relevant input props
};

const Input = ({ label, id, labelIsVisuallyHidden }: InputProps) => {
  return (
    <>
      <label
        htmlFor={id}
        className={labelIsVisuallyHidden ? "visually-hidden" : ""}
      >
        {label}
      </label>
      <input id={id} type="text" />
    </>
  );
};

We have now enforced a11y in our inputs even when labels are not visible to users without sacrificing any of our desired functionality.

Icon buttons

Let's look at a final example - icon buttons. If designed and used correctly, icon buttons are great - they provide a lot of relevant information while allowing us to save valuable screen real estate. But we always need to make sure they are accessible and just as useful to our visually-impaired users.

Let's create a simple IconButton component.


// Icon
const MenuIcon = () => {
  return (
    <svg viewBox="0 0 24 24" width="24px" height="24px" focusable="false">
      <path d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" />
    </svg>
  );
};

// Icon Button
type IconButtonProps = {
  icon: React.ReactNode;
};

const IconButton = ({ icon }: IconButtonProps) => {
  return <button>{icon}</button>;
};

// Usage

const App = () => {
  return (
    <div>
      <IconButton icon={<MenuIcon />} />
    </div>
  );
};

This component works as expected for most users. But it is not accessible. There's currently no way for screen readers to be able to communicate to users what this button is about. We are simply not providing them the necessary information.

Let's rectify the situation.

// Icon
const MenuIcon = () => {
  return (
    <svg
      viewBox="0 0 24 24"
      width="24px"
      height="24px"
      focusable="false"
      aria-hidden={true} // This signals to screen-readers to ignore this element
    >
      <path d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" />
    </svg>
  );
};

// Icon Button
type IconButtonProps = {
  icon: React.ReactNode;
  a11yButtonText: string;
};

const IconButton = ({ icon, a11yButtonText }: IconButtonProps) => {
  return (
    <button>
      {icon}
      <span className="visually-hidden">{a11yButtonText}</span>
    </button>
  );
};

// Usage

const App = () => {
  return (
    <div>
      <IconButton icon={<MenuIcon />} a11yButtonText="menu"/>
    </div>
  );
};

We've added a a11yButtonText prop to our IconButton and a visually hidden span element where the text is displayed for screen readers but not our regular users. By making the prop mandatory, we are also ensuring our component will always be used with accessibility in mind throughout our codebase.

Conclusion

Building websites with accessibility in mind makes them better for everyone. With the power of Typescript, we can make sure we enforce certain accessibility features in our components by default.

But we shouldn't stop there. There's a plethora of a11y tooling we can use to further enhance accessibility on our React app.

Here are just a few examples:

  • eslint-plugin-jsx-a11y - excellent ESLint plugin to statically evaluate JSX to spot accessibility issues
  • @axe-core/react - allows us to test our React components with axe-core accessibility testing library
  • WAVE Web Accessibility Evaluation Tools - a whole suite of accessibility testing tools, including browser extensions that facilitate manual testing

Happy coding! ✨

Did you find this article valuable?

Support Iva Kop by becoming a sponsor. Any amount is appreciated!