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>
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>
Let me break down the above code for you -
- We will first create our css classes, in a
.scss / css
file. - Then we will create a cva function (
flex
) pass our main class and create variants. - Then we create Typescript type for the props (
FlexProps
). - 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.
- 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 -
-
_spacings.scss
, the tokens are available here. -
_radii.scss
, the tokens are available here. -
_fonts.scss
, the tokens are available here. -
_borders.scss
, the tokens are available here. - 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;
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 -
-
dark.scss
tokens are available here -
light.scss
tokens are available here. - 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);
}
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>
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;
}
}
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;
}
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";
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',
....
}
...
}
})
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 }
}
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";
And finally, import the main.scss
file in the src/index.ts
file -
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 -
/** @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. In the next tutorial we will create our first components Box
and Flex
. Until next time PEACE.
Top comments (0)