DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Building a design system using Solidjs, Typescript, SCSS, CSS Variables and Vite - Theme Setup

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. All the code for this tutorial available on GitHub.

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 -

<Box m="md" p="lg" color="white" bg="teal700">
  This is a Box component.
</Box>
Enter fullscreen mode Exit fullscreen mode

To get this similar Developer Experience we will use class-variance-authority, along with static css classes. For example -

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>
Enter fullscreen mode Exit fullscreen mode

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.
  2. _radii.scss, the tokens are available here.
  3. _fonts.scss, the tokens are available here.
  4. _borders.scss, the tokens are available here.
  5. Under _colors.scss paste the following -
$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;
Enter fullscreen mode Exit fullscreen mode

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
  2. light.scss tokens are available here.
  3. Lastly create the themes/main.scss file and paste the following code -
@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);
}
Enter fullscreen mode Exit fullscreen mode

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 -

<Box m="md" p="lg" color="white" bg="teal700">
  This is a Box component.
</Box>
Enter fullscreen mode Exit fullscreen mode

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 -

@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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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 -

@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;
}
Enter fullscreen mode Exit fullscreen mode

In the above snippet we have created atomic classes for our color tokens. Finally import these 2 files in the scss/ main.scss -

/** Import the color theme */
@use "./themes/main.scss";

/** Import utility classes */
@use "./utilities/colors.scss";
@use "./utilities/spacings.scss";
Enter fullscreen mode Exit fullscreen mode

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 -

const paddingVariants = cva([], {
  variants: {
    p: {
      xxs: 'p-xxs',
      ....
    },
    pt: {
      xxs: 'pt-xxs',
      ....
    }
    ...
  }
})
Enter fullscreen mode Exit fullscreen mode

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 -

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 }
} 
Enter fullscreen mode Exit fullscreen mode

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. Finally, under cva-utils/index.ts paste the following -

export * from "./colors";
export * from "./spacing";
Enter fullscreen mode Exit fullscreen mode

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

import './scss/main.scss'

export * from './components/atoms'
Enter fullscreen mode Exit fullscreen mode

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 -

/** @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
Enter fullscreen mode Exit fullscreen mode

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. In the next tutorial we will create our first components Box and Flex. Until next time PEACE.

Top comments (0)