💡 The source code for this post is available on Github: https://github.com/shaikathaque/design-system-tailwind-shadcn-storybook
Table of Contents
Setting up a design system as early as possible can save a lot of time in the long run for an organization and its projects. It is important for engineers to collaborate with designers to establish a design system and library to provide a consistent brand and UX, while improving the developer experience and speed.
This post specifically talks about setting up a design system in a Frontend React project that uses TailwindCSS and Shadcn UI components. The following topics will be covered:
- Storybook
- Theme
- Dark Mode
- Typography
This post won’t be a step by step guide, as technologies change over time and things break. Instead, it provides a big picture, and links to relevant setup guides.
TailwindCSS and Shadcn UI Installation
https://ui.shadcn.com/docs/installation/vite provides instructions to set up TailwindCSS and Shadcn UI in a React with Vite.
In the npx shadcn-ui@latest init step, when asked Do you want to use CSS variables for colors?, choose Yes, as this will allow us to leverage css variables for theming.
The following files are created/modified:
-
components.json- This holds configuration forShadcn UI. This does not need to be updated. You can check https://ui.shadcn.com/schema.json to view itsjsonSchema. -
index.css- This is updated withCSS Variablesthat apply colors.-
:rootselector defines variables for colors to be used by default. -
.darkclass specifies the variables for colors to use when dark mode is enabled.
-
-
tailwind.config.js- This file is updated to extend some of the defaultTailwindCSSstyles with custom styles, such as colors, border radius, etc. TheCSS Variablesdefined inindex.cssare leveraged here.
Storybook Installation
Before we start making any code changes, it is very helpful to setup storybook to easily view changes to our components and theme without writing any layout logic. There are some additional steps needed to make storybook work with Shadcn UI components and dark mode.
⚠️ As of writing, I am using Storybook 8. The syntax and API might change in the future.
🔥 Credit to https://github.com/JheanAntunes/storybook-shadcn for having this setup which I could use as a reference.
Follow the Storybook Installation Instructions: https://storybook.js.org/docs/get-started/install
You should then be able to access storybook by running the storybook script and navigating to http://localhost:6006/. There will be some example components by default. Feel free to remove them.
The following files are added by the initialization script:
-
.storybook/main.ts- This holds storybook configuration such as location of stories, and any add-ons that are installed. -
.storybook/preview.ts- This file allows controlling how stories are rendered, such as adding CSS or decorators for themes. - A
storiesdirectory which will contain our stories.
To get storybook working with Tailwind CSS and Shadcn UI, we must import the global css file in the preview.ts to allow Shadcn UI components to be styled correctly in Storybook.
/* .storybook/preview.ts */
import '../src/index.css';
Now we can start writing stories. Let’s start with a basic button story.
Assuming you have the Shadcn UI button component added with:
npx shadcn-ui@latest add button
Here is a basic button story:
import { Meta, StoryObj } from '@storybook/react';
import { Button } from '@/components/ui/button';
const meta = {
title: 'Atoms/button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: { type: 'select' },
options: [
'default',
'secondary',
'destructive',
'ghost',
'link',
'outline',
],
},
size: {
control: { type: 'select' },
options: ['default', 'icon', 'sm', 'lg'],
},
},
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
// Button Variants
// <Button variant="default">Default</Button>
export const Default: Story = {
args: {
variant: 'default',
children: 'Default',
},
};
// <Button variant="secondary">Secondary</Button>
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary',
},
};
/*
exports for all other variants
*/
Here’s what is going on in this file:
- Following the Atomic Web Design principles, the
titleof themetaobject is set toatoms/button, which will organize thebuttonstories under a section namedAtoms. -
meta- metadata to configure stories for a component. This object is the default export.- here we specify the arguments that can be passed to the shadcn ui components, such as
variant, size, etc.
- here we specify the arguments that can be passed to the shadcn ui components, such as
- Button stories - each object of type
Storythat is exported represents aa story. We can create a story for each way a component is used.
Since the button component renders the label based on the string passed in as its children element, we can specify the label as the children argument.
You should now be able to see the Button stories in Storybook.
Theming
Now that storybook is set up, we can easily explore themes and dark mode in our project.
Theming is set up by shadcn ui out of the box. The css variables in the index.css can be updated for setting the theme. New variables can also be added. You can learn more about it here: https://ui.shadcn.com/docs/theming.
The key thing to keep in mind is that a background and foreground convention is used for naming. However, the background word is not used in the variable name.
For example, given our primary colors are set as:
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--primary will be used as the background color
--primary-foreground will be the text color
Usage:
<div className="bg-primary text-primary-foreground">This is a primary component</div>
This convention is used for all variations - secondary, muted, accent, etc
Shadcn recommends using hsl colors for theming. https://www.smashingmagazine.com/2021/07/hsl-colors-css/
💡 Tip: I highly recommend using VS code extension for highlighting hsl colors. For example, I use the Color Highlight extension to show the color of each hsl variable. After installing, go to extension settings > enable
match hsl with no function. Update marker type to something other than background. I like the option:dot-after
Dark Mode
Shadcn UI provides clear instructions on how to add dark mode to a site: https://ui.shadcn.com/docs/dark-mode. However, additional steps have to be taken to enable Dark Mode in our components in Storybook.
Dark mode works in Shadcn UI but applying a class to the root element in the document tree.
In light mode, the html element will have class="light". In this case, the css variables in the :root of our index.css will be applied.
In dark mode, the html element will have class="dark". In this case, the css variables in the .dark class of our index.css will be applied.
Since storybook does not run within our app, we need to configure add-ons so that the dark and light classes can be applied.
To support this, install the add-on @storybook/addon-themes. Instructions are available here https://github.com/storybookjs/storybook/blob/next/code/addons/themes/docs/getting-started/tailwind.md.
npm i -D @storybook/addon-themes
Then update .storybook.main.ts to configure storybook with this @storybook/addon-themes .
/* .storybook/main.ts */
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
/* existing properties */
addons: [
/* existing add-ons */
'@storybook/addon-themes',
],
/* remaining properties */
};
export default config;
Then, we update .storybook/preview.ts to add remove classes as needed.
/* .storybook/preview.ts */
// existing imports
import { withThemeByClassName } from '@storybook/addon-themes';
import '../src/index.css';
const preview: Preview = {
parameters: {
// existing properties
darkMode: {
classTarget: 'html',
darkClass: 'dark',
lightClass: 'light',
}
},
decorators:[
withThemeByClassName({
themes: {
light: 'light',
dark: 'dark',
},
defaultTheme: 'light'
})
]
};
export default preview;
A few things are happening here:
- We are importing a decorator
withThemeByClassNamefrom@storybook/addon-themeswhich will be used to configure themes in storybook using class names. - We are specifying the following
parametersfordarkMode:-
classTarget- the element to which classes for themes should be applied - in this case thehtmlelement. -
darkClass- the class name to use fordarkmode. -
lightClass- the class name to use forlightmode.
-
- Use the
withThemeByClassNamedecorator to specify the available themes.- In the
themesobject, the key is the theme name, and the value is the theme class name.
- In the
We should now be able to toggle between dark theme and light theme in storybook.
Typography
Setting up typography from the start can save time in the long run rather than copy pasting font styles in multiple places in your code. A design system will ideally have a standardize set of text styles, such as heading, subheading, body, etc which will be reused throughout the UI for a consistent UX.
Since Shadcn UI is not a component library, it does not provide a typography component like other UI libraries such as NextUI, MUI, ChakraUI do. However, it does provide recommendations on how to style text.
I personally like the pattern of having a reusable Text component that can be passed in a variant and overridden with custom styles in one-off scenarios. This is a common scenario and there have been multiple discussion in the Shadcn UI community for such a solution:
Based on the pattern followed in Shadcn UI’s Button component, along with the discussions in Github, here is an example of a Text component for our design system:
/*
./src/components/ui/text.tsx
source: https://github.com/shadcn-ui/ui/pull/363#issuecomment-1659259897
*/
import * as React from 'react';
import { VariantProps, cva } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { Slot } from '@radix-ui/react-slot';
const textVariants = cva('text-foreground', {
variants: {
variant: {
h1: 'scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl',
h2: 'scroll-m-20 pb-2 text-3xl font-semibold tracking-tight',
h3: 'scroll-m-20 text-2xl font-semibold tracking-tight',
h4: 'scroll-m-20 text-xl font-semibold tracking-tight',
p: 'leading-7 [&:not(:first-child)]:mt-6',
lead: 'text-xl text-muted-foreground',
large: 'text-lg font-semibold',
small: 'text-sm font-medium leading-none',
muted: 'text-sm text-muted-foreground',
},
},
defaultVariants: {
variant: "p",
},
});
type VariantPropType = VariantProps<typeof textVariants>;
const variantElementMap: Record<
NonNullable<VariantPropType['variant']>,
string
> = {
h1: 'h1',
h2: 'h2',
h3: 'h3',
h4: 'h4',
p: 'p',
lead: 'p',
large: 'div',
small: 'small',
muted: 'p',
};
type TextElement = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'small' | 'div';
export interface TextProps
extends React.HTMLAttributes<HTMLElement>,
VariantProps<typeof textVariants> {
asChild?: boolean;
as?: TextElement;
}
const Text = React.forwardRef<HTMLElement, TextProps>(
({ className, variant, as, asChild, ...props }, ref) => {
const Comp = asChild ? Slot : as ?? (variant ? variantElementMap[variant] : undefined) ?? 'div';
return (
<Comp
className={cn(textVariants({ variant, className }))}
ref={ref}
{...props}
/>
);
}
);
Text.displayName = 'Text';
export { Text, textVariants };
Example Usage:
<Text variant="h1">Heading H1</Text>
<Text variant="p">Paragraph text</Text>
The variants (h1, h2, p1) can be modified in Text.tsx to meet our design system requirements.
Additionally, if we want to modify Tailwind’s default classes for fontSize (2xl, xl, lg, sm,) we can do so with custom fontSize, lineHeight, letterSpacing, and fontWeight. Instructions can be found here: https://tailwindcss.com/docs/font-size#providing-a-default-line-height.
Add to Storybook
We can document example usage of our Typography components in Storybook.
To do so, first create a story Text.stories.tsx
/* ./src/stories/Text.stories.tsx */
import { Meta, StoryObj } from '@storybook/react';
import { Text } from '@/components/ui/text';
const meta = {
title: 'Text',
component: Text,
argTypes: {
variant: {
control: { type: 'select' },
options: ['h1', 'h2', 'h3', 'h4', 'p', 'small'],
},
as: {
control: { type: 'select' },
options: ['h1', 'h2', 'h3', 'h4', 'h5', 'p'],
},
},
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Text>;
export default meta;
type Story = StoryObj<typeof meta>;
export const H1: Story = {
args: {
variant: 'h1',
children: 'Heading 1',
},
};
export const H2: Story = {
args: {
variant: 'h2',
children: 'Heading 2',
},
};
/* Other variants */
Add an .mdx page to view all Typography components together:
/* ./src/stories/Text.mdx */
import { Canvas, Meta, Primary } from '@storybook/blocks';
import * as TextStories from './Text.stories';
<Meta of={TextStories} />
## Text
The Text component renders text based on a provided variant.
<Primary />
<Canvas of={TextStories.H2} />
<Canvas of={TextStories.H3} />
<Canvas of={TextStories.H4} />
<Canvas of={TextStories.H4} />
<Canvas of={TextStories.P} />
<Canvas of={TextStories.Lead} />
<Canvas of={TextStories.Large} />
<Canvas of={TextStories.Small} />
<Canvas of={TextStories.Muted} />
You should now be able to view the all possible Text variants in Storybook.
Custom Fonts
Documentation on setting custom fonts can be found here: https://tailwindcss.com/docs/font-family#using-custom-values
Download and import the font
You can import fonts from remote target source, or download it to your project. Each has its pros and cons. For this example, I’ll use a downloaded font.
Download .tff file from a trusted source and place in /fonts. Then import the font in index.css using @font-face.
/* index.css */
@layer base {
* {
@apply border-border;
@font-face {
font-family: 'Quicksand';
src: url('/fonts/Quicksand.tff');
}
}
body {
@apply bg-background text-foreground;
}
}
Override default fonts in tailwind.config.js.
/* tailwind.config.js */
const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = {
theme: {
extend: {
fontFamily: {
'sans': ['"Quicksand"', ...defaultTheme.fontFamily.sans],
},
}
}
}
Now, the Quicksand font family will be used by default.
Conclusion
Setting up a design system can take a bit of work but having it set up from the start can increase development speed and code maintainability in the long run. It is important for engineering to work closely with design to ensure coherency in the design system. Especially if the design team uses something like a Shadcn UI figma file as the foundation for the design system, it can vastly improve the development experience.
The source code for this post is available in Github. Please feel free to reach out if there are any questions or confusions about the content of this post, and I will do my best to respond.



Top comments (0)