Icons in a React project

By Paul on April 27, 2020

When I’m working on a project that needs icons, I always reach for Nucleo icons. (No, they’re not paying me. But they are really good.) Both their native and web apps allow for easy exporting of the SVG, but the native app can also export in JSX, which is perfect for this blog which runs on Gatsby, which itself runs on React.

This website’s component structure is pretty straightforward: all the icons are located in src/components/icons, each icon having its own file. For example, the “left arrow” icon is named arrow-left.js. Being JSX, all the icons have a similar structure. For example purposes, I’m going to use one of their free icons. It is a paid product, after all.

import React from 'react';

function Zoom(props) {
	const title = props.title || "zoom";

	return (
		<svg height="24" width="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
			<title>{title}</title>
			<g fill="currentColor">
				<path d="M23.061,20.939l-5.733-5.733a9.028,9.028,0,1,0-2.122,2.122l5.733,5.733ZM3,10a7,7,0,1,1,7,7A7.008,7.008,0,0,1,3,10Z" fill="currentColor"/>
			</g>
		</svg>
	);
};

export default Zoom;

This is fine to start with, but my icon use within the website is often alongside text, like this:

<button type="button">
	<Zoom />
	Search
</button>

In this use case, the icon’s default title will result in a screen reader interpreting the button text as “zoom search,” which would be confusing. So I removed the const title line and modified the title element to include a ternary operator:

{!!props.title &&
	<title>{props.title}</title>
}

This allows the title to only be written if it’s included in the component’s use, like this:

<Zoom title="search" />

In my above example, though, I also don’t want the icon visible to screen readers at all. So I added the aria-hidden property, which also looks at the title:

<svg aria-hidden={!props.title}>

All of this is well and good for each icon, but I have to make these changes all over again whenever I add a new icon. (Okay, it’s not that often, but it’s still tedious.) We can improve this and make it a little more DRY, right? Right?

With that in mind, I created a new file: /src/components/icons.js. Within this file, a single function returns the SVG icon framework:

const icon = (path, className, title) => {
	return (
		<svg className={`icon ${className}`} aria-hidden={!title} height="24" width="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
			{!!title &&
				<title>{title}</title>
			}
			<g fill="currentColor">
				{path}
			</g>
		</svg>
	)
}

It uses the default .icon class (which my CSS framework styles with default height, color, etc.) and accepts additional classes. It also uses the title argument to determine ARIA visibility and the title element. Most importantly, it also accepts a custom path which, of course, determine’s the icon’s appearance.

The file exports all the icons used by my website. To do that, it returns the icon function call:

export const Zoom = (props) => {
	return icon(paths.zoom, `icon--zoom${props.className ? ` ${props.className}` : ''}`, props.title)
}

You’ll notice that the path is not defined here. Instead, I’m calling paths.zoom — the constant paths is defined at the top of the file:

const paths = {
	zoom: <path d="M23.061,20.939l-5.733-5.733a9.028,9.028,0,1,0-2.122,2.122l5.733,5.733ZM3,10a7,7,0,1,1,7,7A7.008,7.008,0,0,1,3,10Z" fill="currentColor"/>,
}

Every time I add a new icon, I copy its path and add it to this object and add a new export. It seems to me to be a little less work than adding a new file and making changes to it, but… I don’t know. I’m open to suggestions.

The other added benefit to managing icons this way is importing them. With the icons all existing in separate files, including multiple icons looked something like this:

import { Heart } from "@icons/heart"
import { Clock } from "@icons/clock"
import { OpenExternal } from "@icons/open-external"

Now, importing multiple icons can be done on a single line:

import { Heart, Clock, OpenExternal } from "@icons"

I guess it’s all about preference. There are many like it, as they say, and this one is mine. And speaking of preferences, I’m also simplifying my imports with the gatsby-plugin-alias-imports plugin. I like it. 👍

© Copyright 2020 - Bold Oak Design LLC