Building a design system with dark mode using React, Typescript, scss, cva and Vite - Design Tokens
Tags: react, css, typescript, vite
Published: 2023-03-20 13:20:08

### Introduction
This is part two of our series on building a complete design system from scratch. In the previous tutorial we bootstrapped the project. In this tutorial we will set up our design tokens for colors, fonts and spacing. Also, we will create atomic css classes for margins, paddings, backgrounds, etc.  I would encourage you to play around with the deployed [storybook](http://yaldram-react-scss-chakra-ui.s3-website.ap-south-1.amazonaws.com/?path=/story/atoms-forms-button--default). All the code for this series is available on [GitHub](https://github.com/yaldram/react-scss-chakra-ui).

### Step One: Getting to know class-variance-authority
Our goal is to create a design system similar to chakra ui, that means our components will take in utility props - 
```tsx
<Box m="md" p="lg" color="white" bg="teal700">
  This is a Box component.
</Box>

<Button colorScheme="teal" variant="solid" size="xs" m="sm">
  Submit 
</Button>
```
Now in our previous design system we used [styled-system](https://styled-system.com/) for these utility props `m, p, color, bg` and we used the [variant function](https://styled-system.com/variants) to create variants for the `Button, Badge and Alert` components. To get this similar DX we will use [class-variance-authority](https://www.npmjs.com/package/class-variance-authority), along with static css classes. For example - 
```tsx
import { cva, cx, VariantProps } from "class-variance-authority";

import "./flex.scss";

const flex = cva(["flex"], {
  variants: {
    direction: {
      row: "flex-row",
      "row-reverse": "flex-row-reverse",
      col: "flex-col",
      "col-reverse": "flex-col-reverse",
    },
  },
});

export type FlexProps = VariantProps<typeof flex>

export const Flex = React.forwardRef<HTMLDivElement, FlexProps>(
  (props, ref) => {
    const {
      direction,
      children,
      ...delegated
    } = props;
    
    return (
      <div ref={ref} className={flex({ direction })} {...delegated}>
        {children}
      </div>
    );
  }
);

<Flex direction="column-reverse"> 
   This is a Flex component.
</Flex>
``` 
Let me break down the above code for you - 
1. We will first create our css classes, in a `.scss / css` file.
2. Then we will create a cva function (`flex`) pass our main class and create variants.
3. Then we create Typescript type for the props (`FlexProps`).
4. We pass the cva function to the className prop by calling it and passing the necessary arguments from our component props, cva function will take care of assigning the necessary class names.
5. We use our component and pass the necessary utility props.

### Step Two: Creating design tokens
In our case design tokens are `scss variables` for colors, spacing, fonts, line-heights, etc. Create a new folder `css` inside the `src` folder. Inside the `css` folder create a new folder `variables` and a file `main.scss` 

Inside the `variables` folder create a new file `_spacings.scss` and paste the following - 
```scss
$spacing-xxs: 0.5rem;
$spacing-xs: 0.8rem;
$spacing-sm: 1rem;
$spacing-md: 1.25rem;
$spacing-lg: 1.5rem;
$spacing-xl: 2rem;
$spacing-xxl: 2.4rem;
$spacing-3xl: 3rem;
$spacing-4xl: 3.6rem;
```
Similarly, create 2 new files `_colors.scss` & `_fonts.scss`, all the tokens for these are [here](https://github.com/yaldram/react-scss-chakra-ui/tree/master/src/css/variables). Take a note that we are using a map for our colors, you will get to know why in the future tutorials.

### Step Three: Creating Spacing and Color classes
Let me re-iterate it again we want to accomplish the following - 
```tsx
<Box m="md" p="lg" color="white" bg="teal700">
  This is a Box component.
</Box>
```
To get these utility props `m, p, color, bg` we have to use cva and create variants. To create these variants we need css classes. So in this section we are going to create `atomic classes` from our design tokens, it's like creating our own small tailwind.css. Because we are not using css in js we can't just pass in dynamic values, we need to create all the css classes in advance at build time.

Under `css` folder create a new file `spacings.scss` and paste - 
```scss
@use "../css/variables/spacings";

@use "sass:meta";

@each $name, $value in meta.module-variables("spacings") {
  /* from $spacing-xs -> xs */
  $token: str-slice($name, 9);

  /* padding classes */
  .p-#{$token} {
    padding: $value;
  }

  .px-#{$token} {
    padding-left: $value;
    padding-right: $value;
  }

  .py-#{$token} {
    padding-top: $value;
    padding-bottom: $value;
  }

  .pt-#{$token} {
    padding-top: $value;
  }

  .pr-#{$token} {
    padding-right: $value;
  }

  .pb-#{$token} {
    padding-bottom: $value;
  }

  .pl-#{$token} {
    padding-left: $value;
  }

  /* margin classes */
  .m-#{$token} {
    margin: $value;
  }

  .mx-#{$token} {
    margin-left: $value;
    margin-right: $value;
  }

  .my-#{$token} {
    margin-top: $value;
    margin-bottom: $value;
  }

  .mt-#{$token} {
    margin-top: $value;
  }

  .mr-#{$token} {
    margin-right: $value;
  }

  .mb-#{$token} {
    margin-bottom: $value;
  }

  .ml-#{$token} {
    margin-left: $value;
  }

  .gap-#{$token} {
    gap: $value;
  }
}
```
We are mapping over our spacing tokens and creating atomic classes like `.pt-xs, .mb-lg, .gap-md`.

Now under `css` folder create a new file `colors.scss` - 
```scss
@use "./variables/colors" as *;

@each $color, $value in $colors-map {
  /* 
    generate color classes for each color
    .color-red500 { color: #E53E3E; }
  */
  .color-#{$color} {
    color: $value;
  }

  /* 
    generate bg color classes for each color
    .bg-red500 { background-color: #E53E3E; }
  */
  .bg-#{$color} {
    background-color: $value;
  }
}
```
In the above snippet we have created `atomic classes` for our color tokens. Finally import these 2 files in the `main.scss` - 
```scss
@use "./colors.scss";
@use "./spacings.scss"
```
And import our main.scss file inside `index.ts` - 
```ts
import "./css/main.scss";

export * from "./components/atoms";
```
This step is important, it tells vite our bundler to transpile main.scss into style.css in the `dist` folder, also we have added some extra config in the vite.config.ts that will give us a `main.css` file inside `dist/css` folder.

### Step Four: Creating cva functions
Now that we have created our atomic classes, let us now create our cva functions. Utility props like margins, paddings will be consumed by many components like `Box, Badge, Button` so we should create the cva functions at one place and use them wherever we need them. Now for padding we need `px, py, pt, pr, pb, pl` props, similar is the case for margin. Each utility prop can take several values, in case of spacing they can be - `xxs, xs, sm, md, lg, xl, xxl, 3xl, 4xl`, which correspond to our design tokens. For padding the cva function will look like - 
```ts
const paddingVariants = cva([], {
  variants: {
    p: {
      xxs: 'p-xxs',
      ....
    },
    pt: {
      xxs: 'pt-xxs',
      ....
    }
    ...
  }
})
``` 
Instead of manually writing all the spacing variants we will create a generic function. But first create a new folder `cva-utils` under `src` and create 3 files namely - `index.ts`, `spacing.ts` and `colors.ts`. Under `spacing.ts` paste - 
```ts
import { cva, VariantProps } from "class-variance-authority";

const spacing = {
  xxs: "0.6rem",
  xs: "0.8rem",
  sm: "1rem",
  md: "1.2rem",
  lg: "1.5rem",
  xl: "2rem",
  xxl: "2.4rem",
  "3xl": "3rem",
  "4xl": "3.6rem",
};

type SpacingMap = Record<keyof typeof spacing, string>;

/**
 * generate cva variants for paddings, margins
 * @classPrefix property eg. p, pt, m, mt, gap, etc.
 * @returns a map of spacing variants
 * eg -{ xs: pt-xs } or { lg: gap-lg }
 */
function generateSpacingMap(classPrefix: string) {
  return Object.keys(spacing).reduce(
    (accumulator, token) => ({
      ...accumulator,
      [token]: `${classPrefix}-${token}`, // xs : p-xs
    }),
    {} as SpacingMap
  );
}

/**
 * Will contain all padding variants
 * eg: p: { xxs: p-xxs, xs: p-xs, ... } &
 * pt: { xxs: pt-xxs, xs: pt-xxs, ... }
 */
export const padding = cva([], {
  variants: {
    p: generateSpacingMap("p"),
    pt: generateSpacingMap("pt"),
    pr: generateSpacingMap("pr"),
    pb: generateSpacingMap("pb"),
    pl: generateSpacingMap("pl"),
  },
});

export type PaddingVariants = VariantProps<typeof padding>;

/**
 * Will contain all margin variants
 * eg: m: { xxs: m-xxs, xs: m-xs, ... }
 * mt: { xxs: mt-xxs, xs: mt-xxs, ... }
 */
export const margin = cva([], {
  variants: {
    m: generateSpacingMap("m"),
    mt: generateSpacingMap("mt"),
    mr: generateSpacingMap("mr"),
    mb: generateSpacingMap("mb"),
    ml: generateSpacingMap("ml"),
  },
});

export type MarginVariants = VariantProps<typeof margin>;

/**
 * Will contain all margin variants
 * eg: gap: { xxs: gap-xxs, xs: gap-xs, ... }
 */
export const flexGap = cva([], {
  variants: {
    gap: generateSpacingMap("gap"),
  },
});

export type FlexGapVariants = VariantProps<typeof flexGap>;

/**
 * Used for storybook controls returns -
 * options: ['xxs', 'xs', 'sm', ...]
 * labels: { xxs: xxs (0.6rem), xs: xs (0.8rem), ... }
 */
export function spacingControls() {
  const spacingOptions = Object.keys(spacing);
  const spacingLabels = Object.entries(spacing).reduce(
    (acc, [key, value]) => ({
      ...acc,
      [key]: `${key} (${value})`,
    }),
    {}
  );

  return { spacingOptions, spacingLabels };
}
```
We are mapping over our spacing tokens and creating the necessary variants. We will follow a similar approach for the color `atomic classes` we map over our color-palette and create the necessary variants, check the code [here](https://github.com/yaldram/solid-vite-lib/blob/feat/theme-setup/src/cva-utils/colors.ts). Finally, under `cva-utils/index.ts` paste the following - 
```ts
export * from "./colors";
export * from "./spacings";
```

And finally import the `main.scss` in the `src/index.ts` file -  
```ts
import './scss/main.scss'

export * from './components/atoms'
```

Now go ahead and build the project, by running `yarn build` from your terminal, check the generated css file, you can find it under `dist/css/main.css`.

Lastly, import the main scss files in `.storybook/preview.tsx` - 
```tsx
import "../src/scss/main.scss";
```

### Conclusion
In this tutorial we created the required design tokens, we created atomic classes and the necessary cva variants for our design system library. All the code for this tutorial can be found [here](https://github.com/yaldram/react-scss-chakra-ui). In the next tutorial we will create our first components `Box` and `Flex`. Until next time PEACE.