May 28, 2024

Creating accessible React icons

When creating a React Icon component, there are many ways to tackle it.

The way we've chosen is through the use of a Higher-Order Component (HOC) and avoiding barrel files (to aid tree shaking).

It requires a new React component to be created each time you create a new icon, but through the use of the HOC, each component will have the right props and be accessible through the use of a title (similar to alt text in an img).

The Higher-Order Component

Below is a HOC named withIcon that takes in an Icon component and an optional default title. It then returns a new Icon component that takes the same props as the original Icon component but also adds an aria-labelledby prop if a title is provided.

tsx

import { useId } from "react";
interface IconProps extends React.SVGProps<SVGSVGElement> {
title?: string;
}
export const withIcon = (IconComponent: React.ComponentType<IconProps>, defaultTitle?: string) => {
const Icon = (props: IconProps) => {
const newProps = { ...props };
const titleId = useId();
const title = newProps.title ?? defaultTitle;
if (title) newProps["aria-labelledby"] = titleId;
return (
<IconComponent {...newProps}>
{title ? <title id={titleId}>{title}</title> : null}
</IconComponent>
);
};
return Icon;
};

Creating an Icon

To create an icon, use the withIcon HOC and pass in the SVG component and a title.

tsx

import { withIcon } from "../icon";
export const PlusIcon = withIcon(({ children, ...props }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="#fff"
strokeWidth="2"
strokeLinecap="round"
{...props}
>
{children}
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
);
});

Using the Icon

To use the icon, import it and use it like any other React component.

The screen reader will read the title of the icon when it's focused.

You now have an accessible icon so that:

  • When the icon is used in an img tag, the title will be used as the alt text.
  • When the icon is used in a button, the title will be used as the aria-label.

tsx

import { PlusIcon } from "./icons/PlusIcon";
const App = () => {
return (
<div>
<PlusIcon title="Increment" />
</div>
);
};

If the icon is purely decorative, you can omit the title prop and use aria-hidden to hide it from screen readers.

tsx

<PlusIcon aria-hidden />

Bundle Size

With this method, you create an icon component per icon and export it using named exports.

You should avoid using barrel files to export all icons at once, as it will prevent tree shaking and increase the bundle size, even when you want to use only one icon.

Conclusion

Using the withIcon Higher-Order Component ensures your React icons are accessible and efficient. Avoid barrel files to enable tree shaking and maintain performance. This approach helps create a more inclusive and optimized web application.