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 thealt
text. - When the icon is used in a
button
, the title will be used as thearia-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.