DEV Community

Paul
Paul

Posted on • Originally published at boldoak.design

Icons in a React project

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 my 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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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

<Zoom title="search" />
Enter fullscreen mode Exit fullscreen mode

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}>
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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"/>,
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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

import { Heart, Clock, OpenExternal } from "@icons"
Enter fullscreen mode Exit fullscreen mode

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

This post was originally published on Bold Oak Design.

Discussion (0)