Back to Blog

Building a Design System; πŸ‘'s & πŸ‘Ž's

8 min read

reactdesign-systembest-practisesintermediate

December 13, 2021 (Updated on January 10, 2022)

Building a Design System; πŸ‘'s & πŸ‘Ž's

Contents


Introduction

Since starting in React, I've encountered similar issues across companies revolving around design systems and how they've been developed. Below, I've listed some common pitfalls, and the right and wrong ways to go about building your design system.

Theming

Tip: Do use an existing theming system. Don't re-invent the wheel.

The React ecosystem has a wide selection of theme systems to choose from, all offering ways to simplify and distribute the styles you create. Here are a few popular ones, some of which I've used in the past:

  • Chakra UI - My personal preference, offers a wide array of default components to choose from, as well as useful utility hooks and an intuitive theming system allowing full control over component theming.
  • Material UI - A very popular component library, offers many components for almost all possible use cases. Downsides involve limited styling, as all components are designed with material design in mind.
  • Theme UI - Another popular design system, offers theme overrides for their bundled components and has a small footprint compared to other packages.
  • Rebass - The most bare-bones of the lot. Not many features are bundled with it, but it offers a good foundation if you need the bare minimum to get going.

Styling

Tip: Do allow component style to be overridden. Don't limit the types of props that can be passed in.

When developing a design system, you can't fully understand the way the end-user will be implementing the components you build. Whilst you might picture a Card component looking one way, the end-user might need something completely different. It's important to incorporate what the user may need into the styles you build.

One solution would be to merge the user-provided styles with ones you've pre-defined. An example of this can be found below, which can be adjusted to your needs if needed:

import { SystemStyleObject } from '@chakra-ui/react';
import { merge } from 'lodash';
import { useMemo } from 'react';

export const useMergedStyles = (sx: SystemStyleObject = {}, styles: SystemStyleObject): SystemStyleObject => {
    return useMemo(() => merge(styles, sx ?? {}), [styles, sx]);
};

The hook has been built with Chakra UI in mind, and takes in two SystemStyleObject objects, and uses the lodash.merge function to deeply merge them. This would be used like so, merging the sx props with the component styles provided within the component:

import { Box, BoxProps, useColorMode } from '@chakra-ui/react';
import React, { FC } from 'react';
import { useMergedStyles } from '~hooks';

export const Card: FC<BoxProps> = ({ children, sx, ...rest }) => {
    const { colorMode } = useColorMode();

    const _sx = useMergedStyles(sx, {
        bg: colorMode === 'dark' ? 'gray.600' : 'gray.200',
    });

    return (
        <Box p="8" rounded="xl" sx={_sx} {...rest}>
            {children}
        </Box>
    );
};

Structure

Tip: Do use one HTML tag per section of your component. Don't hard-code your component structure.

Since React is a component-based framework, it makes sense to retain this structure when building out your component library. Rather than exporting your new components as one single export, split each element of the component into exportable sections.

This allows consumers of your design system to use each piece as needed, as well as allowing them to pass native props to the elements you've built. If you're exporting a button, then all event handlers and styling functions are available to use.

For instance, rather than passing all required content as props like so:

import { Grid,  Heading,  Text, Image } from '@chakra-ui/react';
import React, { FC } from 'react';

export const ThreePanel: FC<ThreePanelProps> = ({ panels }) => {
    return <Grid gap="8" gridTemplateColumns={['1fr', null, '1fr 1fr', null, '1fr 1fr 1fr']} >
        {panels.map(({ title, subtitle, content, image }) => {
            <Grid key={title} gap="8" gridTemplateColumns="1fr" mb="auto" >
                <Heading as="h3" fontSize="3xl" >
                    {title}
                </Heading>
                <Heading as="p" fontSize="xl" variant="subtitle" >
                    {subtitle}
                </Heading>
                <Text>{content}</Text>
                <Image src={image} />
            </Grid>
        })}
    </Grid>
}

Instead, think about exporting the pieces individually, like this:

import { Box, BoxProps, Grid, GridProps, Heading, HeadingProps } from '@chakra-ui/react';
import React, { FC } from 'react';

export const ThreePanel: FC<GridProps> = ({ children, ...rest }) => {
    return (
        <Grid gap="8" gridTemplateColumns={['1fr', null, '1fr 1fr', null, '1fr 1fr 1fr']} {...rest}>
            {children}
        </Grid>
    );
};

export const ThreePanelBlock: FC<GridProps> = ({ children, ...rest }) => {
    return (
        <Grid gap="8" gridTemplateColumns="1fr" mb="auto" {...rest}>
            {children}
        </Grid>
    );
};

export const ThreePanelTitle: FC<HeadingProps> = ({ children, ...rest }) => {
    return (
        <Heading as="h3" fontSize="3xl" {...rest}>
            {children}
        </Heading>
    );
};

export const ThreePanelSubtitle: FC<HeadingProps> = ({ children, ...rest }) => {
    return (
        <Heading as="p" fontSize="xl" variant="subtitle" {...rest}>
            {children}
        </Heading>
    );
};

Individual components will make life so much easier when adding new features, adjusting styling, or re-using elements across components.

Hooks

Tip: Do use hooks to isolate business logic. Don't bundle business logic with your components.

Hooks are a powerful tool to share blocks of code across components, and while developing your design system out, I'd recommend starting hook-first when building out your business logic. Not only does this keep your code isolated, but it also lets consumers of your application re-use the business logic while not being necessarily tied to using the layout you've built.

Additionally, exporting any use of the Context API will make consumers much happier, as they can hook into functionality you've already developed quickly and easily.

Developer Experience

Tip: Do make sure your component is nice to use. Don't focus only on functionality.

When building your components, think about who and how people will be using your component implementation. Whilst functionality is important within a component, making that functionality easily approachable and enjoyable to use is the key to getting that component to stick.

Take the Button component in Chakra UI for example. A bad DX would be requiring styles to be duplicated across a set of buttons:

const MyButtons = () => {
return (<>
  <Button variant="ghost" colorScheme="blue">Content 1</Button>
  <Button variant="ghost" colorScheme="blue">Content 2</Button>
  <Button variant="ghost" colorScheme="blue">Content 3</Button>
  <Button variant="ghost" colorScheme="blue">Content 4</Button>
  <Button variant="ghost" colorScheme="blue">Content 5</Button>
  </>)
}

Whilst a good DX would be providing an easier way to utilize the functionality you've already implemented. Thankfully, Chakra UI has done this with the ButtonGroup component:

const MyButtons = () => {
return (<ButtonGroup variant="ghost" colorScheme="blue">
    <Button>Content 1</Button>
    <Button >Content 2</Button>
    <Button >Content 3</Button>
    <Button>Content 4</Button>
    <Button >Content 5</Button>
</ButtonGroup>)
}

Functionality is important, but how that functionality is expressed is arguably even more important to the end-user.

Testing

Tip: Do ensure your components work for everyone. Don't only test interaction of the component.

Whilst testing, it may be tempting to check purely if the component looks or behaves as it should. This is true, however, there's more to testing than just if it does what it should. Accessibility testing should be something in mind when writing tests; your website should work for those who can and cannot see the actual webpage.

Jest has had an aXe integration available for quite some time, which will check the semantic nature of your components, as well as colour or navigation difficulties that might crop up. Your use of semantic HTML and aria labels is something that can be tested against, so try to make sure your components work for everyone who comes to your site, not only those who can see.

Demonstration

Tip: Do Use Storybook to isolate and demo the components you've built. Don't Demonstrate your new library in-app.

Some companies that I've worked at has tested their components within the app they're being built for. This introduces several issues, one being a bias in terms of how said components will be used, and the other a major slowdown in development turnaround.

Storybook removes both of these issues, allowing standalone development of components in both light and dark modes, responsive development evaluation, and faster turnarounds of components.

Distribution

Tip: Do use peer dependencies for required libraries where possible. Don't bundle all your libraries in your distributed package.

Bundle size is crucial for modern web development, as the larger a site gets, the slower it'll take for your users to start interacting with it. One thing to keep an eye on is the packages you're bundling with your component library. dependencies are bundled on build with your library, so when you publish you'll also be publishing all the packages you've added via NPM. Additionally, if a consuming application is using a different version of a package you've already bundled, you're now bundling two versions of that same library in your site, clearly not ideal.

Instead, think about using peerDependencies. This will tell NPM not to bundle the packages in your distribution, and instead require it to be installed on the consuming application's site. Using version wildcards, we can ensure that only one version of a package is installed at any one time, reducing the total size of your bundles.

Conclusion

In short, build your libraries with modern tooling, and keep flexibility in mind when developing your components. Do this, and you'll find you'll have much less of a headache in 6 months when features have been added left, right, and centre.

Discuss on Twitter
Share on Twitter

Written by Adam Young

Adam Young is a front-end engineer, who specializes in React and modern web technologies. He's working at Checkout.com as a front-end engineer. He currently lives in the the town of Darlington, with his two cats.


Related Articles


Stay up to date

Get the latest articles on web development, technology and best practices, straight to your inbox.


Socials

Contact

Q&AEmail

All rights reserved Β© Adam Young 2022

Source code on Github