💡 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 Variables
that apply colors.-
:root
selector defines variables for colors to be used by default. -
.dark
class specifies the variables for colors to use when dark mode is enabled.
-
-
tailwind.config.js
- This file is updated to extend some of the defaultTailwindCSS
styles with custom styles, such as colors, border radius, etc. TheCSS Variables
defined inindex.css
are 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
stories
directory 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
title
of themeta
object is set toatoms/button
, which will organize thebutton
stories 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
Story
that 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
withThemeByClassName
from@storybook/addon-themes
which will be used to configure themes in storybook using class names. - We are specifying the following
parameters
fordarkMode
:-
classTarget
- the element to which classes for themes should be applied - in this case thehtml
element. -
darkClass
- the class name to use fordark
mode. -
lightClass
- the class name to use forlight
mode.
-
- Use the
withThemeByClassName
decorator to specify the available themes.- In the
themes
object, 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)