Storybook & Atomic Design – 1.7. – SVG Support

SVGs are amazing, they allow us to make use of vector graphics within our applications and have support for CSS properties like stroke and fill allowing us to dynamically change the styles of the icons in our application.

Before we can go about importing SVGs as assets for our components, we have to add some configuration to our Storybook environment to handle the importing of the files.

I've taken a Storybook webpack.config.js example and slightly reworked it to enable SVGs for both Storybook and future-proofing the setup for Gatsby.

Example inspired by - https://github.com/storybookjs/storybook/issues/6188#issuecomment-487705465

webpack.config.js

Create a file named webpack.config.js in the root directory of your Storybook project with the following code snippet.

const path = require(`path`)

module.exports = ({ config }) => {
  let rule = config.module.rules.find(
    r =>
      // it can be another rule with file loader
      // we should get only svg related
      r.test &&
      r.test.toString().includes("svg") &&
      // file-loader might be resolved to js file path so "endsWith" is not reliable enough
      r.loader &&
      r.loader.includes("file-loader")
    )
    rule.test = /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani)(\?.*)?$/

    config.module.rules.push({
      test: /\.svg$/,
      use: ["@svgr/webpack"],
    })

    config.module.rules.unshift({
      test: /\.js$/,
      use: [
        {
          loader: require.resolve("babel-loader"),
          options: { presets: ["react-app"] },
        },
        include: [
        path.join(path.dirname(__dirname), "node_modules/gatsby/cache-dir"),
      ],
  })

  return config
}

If you had Storybook running locally on your machine, you will need to kill the server process from your terminal and restart the Storybook instance to make use of our new webpack configuration.

Our Icon Assets

With the webpack configuration implemented, we can now set about importing our SVG files to our Storybook assets directory.

I've downloaded five SVG icons from https://feathericons.com/

- __Storybook__
   - __assets__
     - __images__
       - __icons__
         - plus.svg
         - shopping-bag.svg
         - shopping-cart.svg
         - user.svg
         - x.svg

Adding Icons to Our Button

To make use of the new icons in our component, we first need to import the files, in the same way, we would import a standard React component.

import React from "react";
import { func, node, string } from "prop-types";

import StyledButton, { StyledLinkButton } from "./button.styles.jsx";

import IconPlus from "../../../assets/images/icons/plus.svg";
import IconBag from "../../../assets/images/icons/shopping-bag.svg";
import IconCart from "../../../assets/images/icons/shopping-cart.svg";
import IconUser from "../../../assets/images/icons/user.svg";
import IconX from "../../../assets/images/icons/x.svg";

With these imported, we can hardcode a specific icon to display within our Button component by using the icon as if it were a React component.

const Button = ({ children, href, onClick, variant }) => {
	if (!href)
		return (
			<StyledButton className="button" onClick={onClick} variant={variant}>
				{children}
				<IconPlus />
			</StyledButton>
		);
	return (
		<StyledLinkButton className="button" variant={variant} href={href}>
			{children}
			<IconPlus />
		</StyledLinkButton>
	);
};

The main issue with this is that we may not always want to use a specific icon in our Button component, and instead, we would want to dynamically swap out the icon being used based on the prop values available to the component.

To implement this dynamic logic, we will instead assign the imported icons to an Icons object in our Button component.

// Assign SVGs to object with named keys
const Icons = {
  bag: IconBag,
  cart: IconCart,
  plus: IconPlus,
  times: IconX,
  user: IconUser
}

Now, we can return an SVG icon when we call a specific key on the object.

e.g. Icons[bag] would return the shopping-bag.svg file to our component render.

In our Button component, as we're creating a sub-component for the Button component, I've separated out some of the logic around the Icon to a component named ButtonIcon which will always expect one prop value (icon) of the type string.

import React from "react";
import { func, node, string } from "prop-types";

import StyledButton, { StyledLinkButton } from "./button.styles.jsx";

import IconPlus from "../../../assets/images/icons/plus.svg";
import IconBag from "../../../assets/images/icons/shopping-bag.svg";
import IconCart from "../../../assets/images/icons/shopping-cart.svg";
import IconUser from "../../../assets/images/icons/user.svg";
import IconX from "../../../assets/images/icons/x.svg";

// Assign SVGs to object with named keys
const Icons = {
	bag: IconBag,
	cart: IconCart,
	plus: IconPlus,
	user: IconUser,
	x: IconX
};

const Button = ({ children, href, icon, onClick, variant }) => {
	if (!href)
		return (
			<StyledButton className="button" onClick={onClick} variant={variant}>
				{children}
				{icon && <ButtonIcon name={icon} />}
			</StyledButton>
		);
	return (
		<StyledLinkButton className="button" variant={variant} href={href}>
			{children}
			{icon && <ButtonIcon name={icon} />}
		</StyledLinkButton>
	);
};

// Expected prop values
Button.propTypes = {
	children: node.isRequired,
	href: string,
	icon: string,
	onClick: func,
	variant: string
};

// Default prop values
Button.defaultProps = {
	children: "Button text",
	variant: "primary"
};

const ButtonIcon = ({ name }) => {
	// If icon name value doesn't match Icons object keys then return null
	if (Icons[name] === undefined) return null;
	// If icon found, return the icon in a span element
	const Icon = Icons[name];
	return (
		<span className="button__icon">
			<Icon />
		</span>
	);
};

// Button Icon component always expects on prop value for icon name
ButtonIcon.propTypes = {
	name: string.isRequired
};

export default Button;

With our new component code, we can call our Button component in the following ways -

import React from "react";
import Button from "./button";

const alertText = e => {
	e.preventDefault();
	alert("You clicked the button");
};

export const basicButton = () => <Button>Basic button</Button>;
export const secondaryButton = () => (
	<Button variant="secondary">Secondary button</Button>
);
export const tertiaryButton = () => (
	<Button variant="tertiary">Tertiary button</Button>
);
// This is where we can change the icon with a prop string on 'icon'
export const removeIconButton = () => (
	<Button icon="times" variant="primary">
		Remove button
	</Button>
);
export const functionButton = () => (
	<Button onClick={alertText}>Click me</Button>
);
export const linkedButton = () => <Button href="/route">Link to route</Button>;

export default {
	component: Button,
	title: "Button"
};

With these variations, we will now want to update our button.stories.js file to document the different ways in which you can use the Button component in a React application.

As you add new props and visual differences to your components, you will want to be reworking and updating your Storybook stories so that anyone contributing to the project can see how a component can be used.

In the next lesson, we'll be covering something I find very exciting, Storybook-addons which will bring your documentation to life and add supporting elements to help create consistent implementations of designed components from your design team.

Continue Reading 📚