DEV Community

Vishnu Gopal
Vishnu Gopal

Posted on

Developing a Design System with React and Tailwind

In this tutorial, we’ll learn some simple techniques for developing scalable design systems using React and utility-first UI framework called Tailwind. Building a design system is not just about choosing the right fonts, spacing, and colours. A good implementation of a design system provides building blocks—like Legos—for engineers to fit components together into usable and delightful products.

Quick Intro: React, and Tailwind

We’ve chosen five tools to flesh out our design system:

  • React is now the most popular Javascript frontend framework out there. With its declarative state-based UI, functional paradigms and—recently—constrained side-effects through Hooks, React if often the first choice to build a frontend application.
  • Storybook is a component visualisation tool: it can display stories of pre-configured components, and can be a powerful tool to build a design system.
  • Tailwind is a new kid on the block—it provides a new way of composing styles using pre-defined CSS classes. With Tailwind, developers often don’t have to write a lot of (or sometimes, any) custom CSS. Tailwind is maturing fast, and has growing developer adoption on Github.
  • Typescript brings type-safety to the null and undefined world of Javascript. It’s flexible enough that interoperability with Javascript is easy, and a growing number of Javascript libraries now ship with Typescript types built in.
  • and finally, styled-components brings to the table a neat way to add styling to components (a CSS-in-JS solution), and some great utility like the ability to quickly switch between different themes.

In the rest of this article, we’ll piece together these three tools to build a sound design system.

Our Design System Deliverables

Let’e first examine what we want as part of a flexible Lego-like design system:

  1. A set of React components that are designed responsive-first. Responsive-design is now a norm, and all our components should be designed mobile-first. Tailwind provides an excellent set of customisable breakpoints, and it makes building responsive layouts easy!
  2. A system to iteratively build and test out these components. You can think of Storybook as an IDE for component development. Components can be developed in isolation, and using a web-based interface, you can even modify its props and get notified on component actions (like a click). It’s a great way to build and maintain a design system.
  3. A consistent & enforceable style guide. Building consistency and enforceabilty to your style guide is where Typescript can really help. As you’ll see in this tutorial, enforcing a consistent set of options for your theme can really mean the difference between a coherent layout, and one that is all over the place.
  4. Self-documenting code, so developers find it easy to use, and hard to make mistakes. We’ll use storybook to ensure that component documentation is in place alongside code. Storybook also provides a visual way to represent this documentation for use by other developers, so that consuming your carefully built components is easy!
  5. All components to have its behaviour well-tested. We’ll use the excellent React Testing Library to test out our components and ensure that component behaviour is tested the same way our users interact with them.

Bootstrap: Installing Everything

To make this tutorial shorter and more focused, there is a bootstrap repo for you to start, clone this repo, checkout to the zero branch (git checkout zero ) and you’ll have everything you need to follow along.

Defining the Work

Because we can’t really build a fully functioning design system from scratch, the scope of this tutorial is to:

  1. Model how to convert designer colour and typography choices into enforceable code
  2. Using that build a simple (but functional and well-tested) component.

The lessons you learn here can be valuable in composing many such components together to build a complete design system.

Typed Themes

A design system usually starts with a colour palette. How do you model that in code? Let’s start by creating a folder src/themes and creating a file called Theme.ts there. This is going to be our type definition file for our design system:

    //src/themes/Theme.ts

    interface Theme {
        [key: string]: string | number;
        name: string;
        neutralColor: string;
        actionColor: string;
        secondaryActionColor: string;
        dangerColor: string;
        warningColor: string;
        successColor: string;
    }
    export default Theme;
Enter fullscreen mode Exit fullscreen mode

Note that the first [key: string]: string | number definition is just so that we can access theme keys using an index notation, like theme["actionColor"]. As we’ll see later in the article, this makes accessing properties simpler when you pass in semantic roles to your components.

Now we can then make a concrete implementation of this theme by building a concrete lightTheme.ts in the same folder, and defining these colours in terms of their Tailwind notation:

    //src/themes/lightTheme.ts

    import Theme from './Theme';
    const lightTheme: Theme = {
        name: 'Light',
        neutralColor: 'gray',
        actionColor: 'blue',
        secondaryActionColor: 'purple',
        dangerColor: 'red',
        warningColor: 'yellow',
        successColor: 'green',
    };
    export default lightTheme;
Enter fullscreen mode Exit fullscreen mode

We’re borrowing inspiration from the Clarity design system here, but these colours can be anything decided by your design team. Tailwind has a mechanism for extensive customisation of colours, and indeed, pretty much anything. For the purposes of this article, we’ll stick to the basic Tailwind palette.

Note that in Tailwind, colours are made up of 2 values, a shade (grey, purple, and so on), and an intensity level (from 100-900), that is very similar to the lightness parameter in the LCH colour scheme. So to model a complete colour for an interaction state (such as a default state, hover state, a selected state and so on), you need both a shade, and an intensity. Since the shade is decided by the role of the element, the intensity can decide how it’ll change based on interaction state. That gives us a pretty elegant theme design:

    interface Theme {
      ...
      defaultIntensity: number;
      hoverIntensity: number;
      selectedIntensity: number;
    }
Enter fullscreen mode Exit fullscreen mode

and:

    const lightTheme = {
      ...
      defaultIntensity: "200",
      hoverIntensity: "300",
      selectedIntensity: "600"
    }
Enter fullscreen mode Exit fullscreen mode

Now let’s look at building a component with this typed theme.

Building a Button Component

We’re going to build a simple Button component using the above theme definition. To do that, create a file called Button.tsx in src/.

    // src/Button.tsx

    import React from 'react';
    import classNames from 'classnames';
    import { withTheme } from 'styled-components';
    import Theme from '../themes/Theme';

    interface ButtonProps {
        title: string;
        role?: 'action' | 'secondaryAction';
        onClick?: () => void;
    }
    type Props = { theme: Theme } & ButtonProps;

    const Button: React.FC<Props> = ({ theme, title = 'Button', role = 'action', onClick }) => {
        const tint = theme[`${role}Color`];
        const defaultBackgroundColor = `${tint}-${theme.defaultIntensity}`;
        const hoverBackgroundColor = `${tint}-${theme.hoverIntensity}`;
        const borderColor = `${theme.neutralColor}-${theme.defaultIntensity}`;
        const defaultTextColor = `${tint}-${1000 - theme.defaultIntensity}`;
        const hoverTextColor = `${tint}-${1000 - theme.hoverIntensity}`;
        const backgroundClasses = [`bg-${defaultBackgroundColor}`, `hover:bg-${hoverBackgroundColor}`];
        const textClasses = [`font-bold text-${defaultTextColor}`, `hover:text-${hoverTextColor}`];
        const borderClasses = [`rounded`, `border-${borderColor}`, `border-1`];
        const paddingAndMarginClasses = [`py-2 px-4`];
        return (
            <button
                className={classNames(
                    backgroundClasses,
                    textClasses,
                    borderClasses,
                    paddingAndMarginClasses
                )}
                onClick={onClick}
            >
                {title}
            </button>
        );
    };
    export default withTheme(Button);
Enter fullscreen mode Exit fullscreen mode

There’s a bit to parse here, so let’s take this line by line:

  1. In lines 3-6 we import the default React import (so that .tsx files can see the JSX syntax), the classnames library which makes composing classes much easier, and the withTheme helper from styled-components that makes theming a component as easy as exporting a higher-order component wrapped in withTheme. We also import our created Theme.ts type definition.
  2. In lines 8-13, we type out our props necessary for Button: a title that is displayed on the button, the role, either a primary action or a secondaryAction, and an onClick function handler. We also make sure to add our theme prop that is passed in by styled-components so that our theme properties are accessible inside the component.
  3. Lines 16-25 are where we define the actual colour classes to use in the button. Let’s work through these colours assuming that the role is action. The tint constant becomes theme["actionColor"] which is defined in our lightTheme as blue. The defaultBackgroundColor then becomes blue-200, a valid Tailwind colour. Note how on line 20, we use a basic understanding of color theory to derive the text color by subtracting 1000 from the default background intensity to give a pleasing contrast. So defaultTextColor becomes blue-800. Note that if this is optional: if your designer insists on a different text colour, you can very well use that here.

We’re also going to create a corresponding Storybook story for this component in stories/

    // src/stories/Button.stories.tsx

    import React from 'react';
    import Button from '../components/Button';
    import { withKnobs, text } from '@storybook/addon-knobs';
    import { action } from '@storybook/addon-actions';
    export default {
        title: 'Button',
        component: Button,
        decorators: [withKnobs],
    };
    export const Action = () => (
        <div className="m-2">
            <Button
                title={text('Button title', 'Login')}
                role="action"
                onClick={action('button-click')}
            />
        </div>
    );
    export const SecondaryAction = () => (
        <div className="m-2">
            <Button
                title={text('Button title', 'Login')}
                role="secondaryAction"
                onClick={action('button-click')}
            />
        </div>
    );
Enter fullscreen mode Exit fullscreen mode

This is a standard storybook file with some addons configured: we have a text knob here for button title, and two variants Action and SecondaryAction that tweaks the roles accordingly. Let’s now run yarn storybook and see how this component looks:

Viewing the Button story in Storybook

Note that Storybook also provides a lot of conveniences for building a component. Let’s say you want to build a responsive component, there is a viewport add-on configured in the project that helps you see a mobile layout:

Viewing in a mobile layout.

Step 3: Conclusion & Thoughts

In this article, we learnt how to build a simple Component using a typed design system. To build a next component, and then to build layouts and pages on top of that component, here are the steps you should follow:

  1. First, look at your Theme definition in Theme.ts, and see if there are any new design system parameters to be added. Perhaps you are building a table for the first time, and that requires a different row and column colour. Configure these parameters in the Theme type definition and in the concrete implementations such as lightTheme based on design input.
  2. Next, start by writing a simple Story for your component in src/stories and configuring a simple default story so that you can view this component in Storybook.
  3. Now, build out your component in src/. If your component is complex and relies on importing other components, feel free! React is really great at composition, and it’s a great way to provide pre-built layouts and pages to your developers.
  4. That’s it really! You can experiment in the project with building more themes, and switching with them right within Storybook. Storybook also provides a Docs tab that has some great auto-generated documentation for your developers to use.

Have fun, and happy theming!

Latest comments (0)