Building a design system using Solidjs, Typescript, SCSS, CSS Variables and Vite - Theme Setup
Tags: solidjs, typescript, css, vite
Published: 2023-04-15 12:21:56

### Introduction
This is part two of our series on building a design system using Solidjs. 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://solid-vite-lib.s3-website.ap-south-1.amazonaws.com/?path=/story/atoms-forms-button--playground). All the code for this tutorial available on [GitHub](https://github.com/yaldram/solid-vite-lib).

### Step One: Getting to know class-variance-authority
I am a huge fan of Chakra Ui and its use of `utility props` like you can pass in `padding, margin, background, color` values as props to your components - 
```tsx
<Box m="md" p="lg" color="white" bg="teal700">
  This is a Box component.
</Box>
```
To get this similar Developer Experience 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 this component and pass the necessary utility props.

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

1. `_spacings.scss`, the tokens are available [here](https://github.com/yaldram/solid-vite-lib/blob/master/src/scss/variables/_spacings.scss).
2. `_radii.scss`, the tokens are available [here](https://github.com/yaldram/solid-vite-lib/blob/master/src/scss/variables/_radii.scss).
3. `_fonts.scss`, the tokens are available [here](https://github.com/yaldram/solid-vite-lib/blob/master/src/scss/variables/_fonts.scss).
4. `_borders.scss`, the tokens are available [here] (https://github.com/yaldram/solid-vite-lib/blob/master/src/scss/variables/_borders.scss).
5. Under `_colors.scss` paste the following - 
```scss
$color-list: blue, cyan, gray, green, pink,  purple, red, yellow;

$color-shades: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900;

$color-schemes: neutral, primary, secondary, success, warning, error;
```

Now under `scss` folder create a new folder called `themes`, in this folder we will create our color CSS variables for both the light and dark modes. Inside `scss/themes` folder create the following files - 

1. `dark.scss` tokens are available [here](https://github.com/yaldram/solid-vite-lib/blob/master/src/scss/themes/dark.scss)
2. `light.scss` tokens are available [here](https://github.com/yaldram/solid-vite-lib/blob/master/src/scss/themes/light.scss).
3. Lastly create the `themes/main.scss` file and paste the following code - 
```scss
@use "./dark.scss";
@use "./light.scss";

.root {
  --color-white: #ffffff;
  --color-black: #000000;

  /* Primary colors */
  --color-primary-light: var(--blue200);
  --color-primary-light-hover: var(--blue300);
  --color-primary-light-active: var(--blue400);
  --color-primary-light-contrast: var(--blue600);

  --color-primary: var(--blue600);
  --color-primary-border: var(--blue500);
  --color-primary-border-hover: var(--blue600);
  --color-primary-solid-hover: var(--blue700);
  --color-primary-solid-contrast: var(--color-white);
  --color-primary-shadow: var(--blue500);

  --color-secondary-light: var(--purple200);
  --color-secondary-light-hover: var(--purple300);
  --color-secondary-light-active: var(--purple400);
  --color-secondary-light-contrast: var(--purple600);

  /* Secondary colors */
  --color-secondary: var(--purple600);
  --color-secondary-border: var(--purple500);
  --color-secondary-border-hover: var(--purple600);
  --color-secondary-solid-hover: var(--purple700);
  --color-secondary-solid-contrast: var(--color-white);
  --color-secondary-shadow: var(--purple500);

  /* Success colors */
  --color-success-light: var(--green200);
  --color-success-light-hover: var(--green300);
  --color-success-light-active: var(--green400);
  --color-success-light-contrast: var(--green700);

  --color-success: var(--green600);
  --color-success-border: var(--green500);
  --color-success-border-hover: var(--green600);
  --color-success-solid-hover: var(--green700);
  --color-success-solid-contrast: var(--color-white);
  --color-success-shadow: var(--green500);

  /* Warning colors */
  --color-warning-light: var(--yellow200);
  --color-warning-light-hover: var(--yellow300);
  --color-warning-light-active: var(--yellow400);
  --color-warning-light-contrast: var(--yellow700);

  --color-warning: var(--yellow600);
  --color-warning-border: var(--yellow500);
  --color-warning-border-hover: var(--yellow600);
  --color-warning-solid-hover: var(--yellow700);
  --color-warning-solid-contrast: var(--color-white);
  --color-warning-shadow: var(--yellow500);

  /* Error colors */
  --color-error-light: var(--red200);
  --color-error-light-hover: var(--red300);
  --color-error-light-active: var(--red400);
  --color-error-light-contrast: var(--red600);

  --color-error: var(--red600);
  --color-error-border: var(--red500);
  --color-error-border-hover: var(--red600);
  --color-error-solid-hover: var(--red700);
  --color-error-solid-contrast: var(--color-white);
  --color-error-shadow: var(--red500);

  /* Neutral colors */
  --color-neutral-light: var(--gray100);
  --color-neutral-light-hover: var(--gray200);
  --color-neutral-light-active: var(--gray300);
  --color-neutral-light-contrast: var(--gray800);

  --color-neutral: var(--gray600);
  --color-neutral-border: var(--gray400);
  --color-neutral-border-hover: var(--gray500);
  --color-neutral-solid-hover: var(--gray600);
  --color-neutral-solid-contrast: var(--color-white);
  --color-neutral-shadow: var(--gray400);

  --color-gradient: linear-gradient(112deg, var(--cyan600) -63.59%, var(--pink600) -20.3%, var(--blue600) 70.46%);

  /* Accents */
  --color-accents0: var(--gray50);
  --color-accents1: var(--gray100);
  --color-accents2: var(--gray200);
  --color-accents3: var(--gray300);
  --color-accents4: var(--gray400);
  --color-accents5: var(--gray500);
  --color-accents6: var(--gray600);
  --color-accents7: var(--gray700);
  --color-accents8: var(--gray800);
  --color-accents9: var(--gray900);
}
```
Basically in the `dark.scss` & `light.scss` files we have our color palette for dark and light modes respectively and under the `scss/themes/main.scss` we have the design tokens for our colors. 

When we use our component library in a project, we need to add the `.root` and `.light-theme / .dark-theme` classes to the root of our project.

### Step Three: Creating utility 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 the `scss` folder create another folder `utilities` inside it create a new file `spacings.scss` - 
```scss
@use "../variables/spacings";

@use "sass:meta";

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

  /* 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 classes */
  .gap-#{$token} {
    gap: $value;
  }
}
```
We are mapping over our spacing tokens and creating atomic classes like `.pt-xs, .mb-lg, .gap-md`.

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

@use "sass:meta";

@each $color in $color-list {
  @each $shade in $color-shades {
    $colorShade: #{"" + $color + $shade};

    .color-#{$colorShade} {
      color: var(--#{$colorShade});
    }

    .bg-#{$colorShade} {
      background-color: var(--#{$colorShade});
    }
  }
}

.color-white {
  color: white;
}

.bg-white {
  background-color: white;
}

.color-black {
  color: black;
}

.bg-black {
  background-color: black;
}
```
In the above snippet we have created `atomic classes` for our color tokens. Finally import these 2 files in the `scss/ main.scss` - 
```scss
/** Import the color theme */
@use "./themes/main.scss";

/** Import utility classes */
@use "./utilities/colors.scss";
@use "./utilities/spacings.scss";
```

### 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 `spacings.ts` paste - 
```ts
import { cva, VariantProps } from 'class-variance-authority'

/* spacing tokens */
const spacing = {
  0: '0rem',
  xs: '0.5rem',
  sm: '0.75rem',
  md: '1rem',
  lg: '1.25rem',
  xl: '2.25rem',
  '2xl': '3rem',
  '3xl': '5rem',
  '4xl': '10rem',
  '5xl': '14rem',
  '6xl': '18rem',
  '7xl': '24rem',
  '8xl': '32rem',
  '9xl': '40rem',

  min: 'min-content',
  max: 'max-content',
  fit: 'fit-content',

  screen: '100vw',
  full: '100%',
  px: '1px',

  1: '0.125rem',
  2: '0.25rem',
  3: '0.375rem',
  4: '0.5rem',
  5: '0.625rem',
  6: '0.75rem',
  7: '0.875rem',
  8: '1rem',
  9: '1.25rem',
  10: '1.5rem',
  11: '1.75rem',
  12: '2rem',
  13: '2.25rem',
  14: '2.5rem',
  15: '2.75rem',
  16: '3rem',
  17: '3.5rem',
  18: '4rem',
  20: '5rem',
  24: '6rem',
  28: '7rem',
  32: '8rem',
  36: '9rem',
  40: '10rem',
  44: '11rem',
  48: '12rem',
  52: '13rem',
  56: '14rem',
  60: '15rem',
  64: '16rem',
  72: '18rem',
  80: '20rem',
  96: '24rem'
}

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 color 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'),
    px: generateSpacingMap('px'),
    py: generateSpacingMap('py'),
    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'),
    mx: generateSpacingMap('mx'),
    my: generateSpacingMap('my'),
    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/react-scss-chakra-ui/blob/master/src/cva-utils/colors.ts). Finally, under `cva-utils/index.ts` paste the following - 
```ts
export * from "./colors";
export * from "./spacing";
```

And finally, import the `main.scss` file 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` and attach the root and theme classes - 
```tsx
/** @jsxImportSource solid-js */

import '../src/scss/main.scss'

const preview = {
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/
      }
    }
  }
}

export const decorators = [
  (StoryFun) => (
    <div class='root light-theme'>
      <StoryFun />
    </div>
  )
]

export default preview
```

### 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/solid-vite-lib). In the next tutorial we will create our first components `Box` and `Flex`. Until next time PEACE.