Simple Storybook React Setup with Dark Mode Integration
Storybook is a handy tool for managing insight into an application’s component system. It renders components in an isolated context — each state of the component displayed as its own “story” — and can be installed in a production repository while not getting in the way of the actual code. And it’s simple to set up and use, flexible, and extensible.
It has an official plugin architecture called “addons” which are used to add functionality to the main Storybook application and maintains a few key concepts and special files to attach extra items and functionality to components without polluting the actual component code with any special scaffolding or helpers.
Storybook has thorough documentation, and the basic set up is very straight-forward. What this tutorial covers is how to configure a dark mode toggle for Storybook that also switches the context of the React components in the stories to match. (There’s also a bonus section at the end that covers how to set up custom webfonts, which I found to be slightly confusing to set up at first.)
The outcome will be this:
We will use the storybook-dark-mode addon to enable the dark mode option for Storybook, and then use a React hook provided by the addon to connect this dark mode toggle to our React components. We will then use Emotion’s styled
function and ThemeProvider
component to facilitate the theme switching for the components.
Note that this tutorial is specifically for React. Storybook can also be used in Vue, Angular and many other projects. See their documentation section for more on those. This tutorial assumes that you are familiar with yarn
and Terminal and that React components are not completely foreign to you.
tldr
The code from this tutorial in its final form is on GitHub.
Table of Contents
1 Set Up New Project and Add Dependencies
To begin, make a new directory called storybook-react-with-dark-mode
wherever you normally put your development projects. This directory will contain everything we are creating and installing in this tutorial: a few React components, a very minimal example of a design system, our stories for Storybook, and all the project dependencies.
Once this directory as been created, open Terminal and navigate to the directory that was just created:
bashcd path/to/storybook-react-with-dark-mode
Now let’s install the dependencies we’ll need. We are going to install Storybook and React and a couple Babel packages that are required as peer dependencies. Also I should note that we are “manually” installing Storybook. Storybook offers an automatic setup for React but we’ll be going through the manual method so we can see how Storybook is set up and works within a React project.
We’ll install Storybook and Babel as dev-dependencies and React as a dependency. I’m using yarn
but you can use npm
if you prefer. Just change the commands accordingly (e.g. npm install <package>
). First run this command:
bashyarn add -D @storybook/react babel-loader @babel/core
Then:
bashyarn add react react-dom
Your directory should now look like this:
txtstorybook-react-with-dark-mode/
├─ /node_modules
├─ package.json
└─ yarn.lock
I know I said I wasn’t going to be covering the source control part of this, but I am going to add a quick .gitignore file to the project to exclude the node_modules
directory. You can optionally add this file to your project directory as well.
Create a new file named .gitignore
and add this as its contents:
.gitignore.DS_Store
node_modules
npm-debug.log
yarn-error.log
So with the .gitignore file added our project directory now looks like this:
txtstorybook-react-with-dark-mode/
├─ /node_modules
├─ .gitignore
├─ package.json
└─ yarn.lock
The last thing we’ll do is add Emotion to the project. We are going to use Emotion to style our components and also to manage the dark mode theme switching. You can use any CSS-in-JS solution that supports theming. I like to use Emotion, but the syntax for styled-components for example should be nearly identitical (I think).
Add Emotion with yarn
like so:
bashyarn add @emotion/core @emotion/styled emotion-theming
Now we have the tools we need for the first part of the tutorial; the next step is to configure them all.
2 Configure Storybook
We are going to configure two things: we’ll first set up the code that Storybook uses to find the “stories” to display, and then we’ll add a command in our package.json
to run Storybook. Don’t worry about understanding stories completely at the moment, we will go over them in more detail shortly.
Create a new directory named .storybook
(note the preceding .
) in your project directory and in this directory create a new file named main.js
. Your project directory should look like this now:
txtstorybook-react-with-dark-mode/
├─ /.storybook
| └─ main.js
├─ /node_modules
├─ .gitignore
├─ package.json
└─ yarn.lock
In main.js
add the following code:
jsmodule.exports = {
stories: ['../src/**/*.stories.[tj]s']
}
What this does is tell Storybook to collect any files that have .stories.js
or .stories.ts
(if you’re using Typescript) in the name and then pass them along to be processed as stories.
Note that we tell Storybook to look in a src
directory — this hasn’t been created yet but it’s where we will place our React components. Storybook recommends placing the story files in the same location as the components of the stories they tell. But you don’t have to, you can place them anywhere in the project, and you don’t have to name them with the convention of .stories
either, it could be whatever you wanted. That said, I would recommend following the conventions and in this tutorial we’ll be doing just that.
Next we need to add a command to our package.json file that initiates the Storybook process; and while we’re doing that we’ll add a few more things to fill out the file. Match your package.json
file to what’s shown below.
json{
"name": "storybook-react-with-dark-mode",
"version": "0.1.0",
"scripts": {
"storybook": "start-storybook -p 9001"
},
"devDependencies": {
"@babel/core": "^7.11.1",
"@storybook/react": "^6.0.1",
"babel-loader": "^8.1.0"
},
"dependencies": {
"@emotion/core": "^10.0.34",
"@emotion/styled": "^10.0.27",
"emotion-theming": "^10.0.27",
"react": "^16.13.1",
"react-dom": "^16.13.1"
}
}
We added a name and version to our project and then one script named storybook
. Now when we run yarn storybook
from the Terminal in the project directory it will in turn run start-storybook -p 9001
.
The -p 9001
part is optional; what it does is tell Storybook to run on port 9001. So every time you run Storybook it will load at http://localhost:9001
. If you don’t have that option specified it will load on a different port number each time.
If you tried to run the storybook
command you would get an error because we haven’t created the src
directory yet. Let’s now add that directory and a few React components.
3 Add React Components
In this section we’ll add four very basic React components: Input, Button, Label, and Card. The first three will be single HTML elements with associated styles; the Card component we’ll make a composite component so we can experience a more dynamic version of toggling the dark mode on and off.
We will create these components with hard-coded styles first and then return later to connect the styling to a theming system.
Let’s create the Input
component first. In your project directory, create a new directory named src
and then under the src directory create a subdirectory named components
and then in the components subdirectory create another subdirectory named Input
and add two files to it: index.js
and Input.component.js
. The new project structure should look like this:
txtstorybook-react-with-dark-mode/
├─ /.storybook
| └─ main.js
├─ /node_modules
├─ /src
| └─ /components
| └─ /Input
| ├─ index.js
| └─ Input.component.js
├─ .gitignore
├─ package.json
└─ yarn.lock
This is my preferred way of organizing components and is not a required structure. I would recommend just following it for now and then adjusting to your perferred setup after you understand how all the pieces fit together.
In the Input/index.js
file add the following:
jsimport Input from './Input.component'
export default Input
This provides a more succinct import path for other parts of the app that need to import the component.
In Input/Input.component.js
add the following:
jsximport React from 'react'
import styled from '@emotion/styled'
const StyledInput = styled.input`
box-sizing: border-box;
display: block;
width: 290px;
height: 40px;
padding: 0 12px;
font-family: sans-serif;
font-size: 16px;
font-style: ${props => props.disabled ? 'italic' : 'normal'};
line-height: 1.1;
color: ${props => props.disabled ? '#888' : '#222'};
border-width: ${props => props.disabled ? 0 : '1px'};
border-style: solid;
border-color: #e0e0e0;
border-radius: 4px;
background-color: ${props => props.disabled ? '#f5f5f5' : '#fff'};
outline: none;
&:active, &:focus {
border-color: ${props => props.disabled ? '#e0e0e0' : '#33e'};
background-color: ${props => props.disabled ? '#f5f5f5' : '#fff'};
}
&::placeholder {
color: #888;
}
`
const Input = ({ children, disabled, ...rest }) => (
<StyledInput
{...{ disabled }}
{...rest}
/>
)
export default Input
I won’t go into what’s happening in that component in order to stay focused on the Storybook aspect of this tutorial, but one thing that may look odd is the spread operators on StyledInput: {...{ disabled }}
is the same as writing disabled={disabled}
and {...rest}
just copies over any props that were not extracted explicitly. More on spread operators.
Now let’s create the other three components following the same pattern as before. The src
directory should look like this when you are finished:
txtsrc
└─ /components
└─/Button
| ├─ index.js
| └─ Button.component.js
└─/Card
| ├─ index.js
| └─ Card.component.js
└─/Input
| ├─ index.js
| └─ Input.component.js
└─/Label
├─ index.js
└─ Label.component.js
And here is the content for each of the new files:
Button/index.js
jsimport Button from './Button.component'
export default Button
Button/Button.component.js
jsimport React from 'react'
import styled from '@emotion/styled'
const StyledButton = styled.button`
`
const Button = ({ children, disabled, ...rest }) => (
<StyledButton
{...{ children, disabled }}
{...rest}
/>
)
export default Button
Card/index.js
jsimport Card from './Card.component'
export default Card
Card/Card.component.js
jsimport React from 'react'
import styled from '@emotion/styled'
const StyledCard = styled.div`
`
const Card = ({ children }) => (
<StyledCard {...{ children }} />
)
export default Card
Label/index.js
jsimport Label from './Label.component'
export default Label
Label/Label.component.js
jsimport React from 'react'
import styled from '@emotion/styled'
const StyledLabel = styled.label`
`
const Label = ({ children, disabled, ...rest }) => (
<StyledLabel
{...{ children, disabled }}
{...rest}
/>
)
export default Label
Now that we have our src
directory and components let’s try running Storybook, in Terminal run:
bashyarn storybook
In the Terminal, you should see some loading messages, then a Storybook started message block and then http://localhost:9001 will probably open up automatically in your default browser. If it doesn’t, just open a new tab and go to that URL. You should see a blank Storybook view with a message about not finding any stories.
We have created components, but Storybook doesn’t know about them yet because there are no .stories.js
files. In the next section we’ll create our story files.
4 Adding Stories
Remember from Section 2 that we told Storybook to look for stories in any file in the src
directory that has .stories
in it. So to hook into Storybook we only need to create a file that matches that pattern and include any necessary code in this file. Let’s create a story for our Button component first. In the Button directory create a new file alongside the other two named Button.stories.js
. The Button directory should look like this now:
txtsrc
└─ /components
└─/Button
| ├─ index.js
| ├─ Button.component.js
| └─ Button.stories.js
…
And then in Button.stories.js
add the following content:
jsimport React from 'react'
import Button from './'
export default {
title: 'Button'
}
export const Basic = () => (
<Button>Submit</Button>
)
If you go back to Storybook you should see the Button component:
(Storybook automatically reloads any time it detects changes to the story files. Or in our case when it finds a new file.)
In the sidebar you’ll see the parent nav item is named “Button”; this comes from the export default
in the stories file. And then beneath that is the individual story link named “Basic”, which is derived from the export const
name. Each exported variable in the stories file is treated as an individual story in Storybook. Let’s create another one so it’s easier to see.
In the Button component we added the ability to pass a disabled prop which would change the styling of the Button to reflect this disabled state. Let’s add a story that shows this disabled state. Update Button.stories.js
to match below:
jsimport React from 'react'
import Button from './'
export default {
title: 'Button'
}
export const Basic = () => (
<Button>Submit</Button>
)
export const Disabled = () => (
<Button disabled>Submit</Button>
)
Save the updated file and view Storybook. You should now see another story named “Disabled” and if you click on it you should see our disabled button.
Storybook will transform the name of our variable from camelCase (or PascalCase) to Title Case. So if we named a story export const myFavoriteComponent
the story link in Storybook would be “My Favorite Component”.
A side note: if you ever have a situation where you need to export a variable but don’t want it as a story you can explicitly specify which of the exports are stories by passing an includeStories
array in the export default
object. So in our case that would look like this:
jsexport default {
title: 'Button',
includeStories: [
'Basic',
'Disabled'
]
}
But for now you needn’t worry about this.
Let’s add the stories for the other components. Following the same pattern as Button.stories.js add three more .stories files, one for each of the remaining components:
txtsrc
└─ /components
└─/Button
| ├─ index.js
| ├─ Button.component.js
| └─ Button.stories.js
└─/Card
| ├─ index.js
| ├─ Card.component.js
| └─ Card.stories.js
└─/Input
| ├─ index.js
| ├─ Input.component.js
| └─ Input.stories.js
└─/Label
├─ index.js
├─ Label.component.js
└─ Label.stories.js
And then the content for each:
Card/Card.stories.js
jsimport React from 'react'
import Button from '../Button'
import Input from '../Input'
import Label from '../Label'
import Card from './'
export default {
title: 'Card'
}
export const Basic = () => (
<Card>
<Label>Name</Label>
<Input placeholder="First name" style={{ marginTop: '4px' }} />
<Input placeholder="Last name" style={{ marginTop: '4px' }} />
<Label style={{ marginTop: '12px' }}>Email</Label>
<Input type="email" placeholder="Email address" style={{ marginTop: '4px' }}/>
<Label style={{ marginTop: '12px' }}>Password</Label>
<Input type="password" placeholder="Password" style={{ marginTop: '4px' }} />
<Input type="password" placeholder="Re-enter password" style={{ marginTop: '4px' }} />
<Button style={{ marginTop: '12px' }}>Sign Up</Button>
</Card>
)
Input/Input.stories.js
jsimport React from 'react'
import Input from './'
export default {
title: 'Input'
}
export const Basic = () => (
<Input placeholder="Email address" />
)
export const Disabled = () => (
<Input disabled placeholder="Email address" />
)
Label/Label.stories.js
jsimport React from 'react'
import Label from './'
export default {
title: 'Label'
}
export const Basic = () => (
<Label>Email</Label>
)
export const Disabled = () => (
<Label disabled>Email</Label>
)
Once you’ve added all the stories, Storybook should look like this:
At this point we have Storybook installed, configured, and working. The rest of the tutorial will center around the functionality and pieces required to add a dark mode option for our Storybook setup and components.
5 Add Design Tokens and a Theme File
In order to take advantage of switching to dark mode in Storybook, our components must first themselves be able to change styles based on context. To do this we are going to create a mini makeshift design system with a few design tokens and then use Emotion to replace all of our hard-coded styles with their design token counterparts.
Let’s first create our mini design system by creating a few files to house some design tokens. In the src
directory, create a new directory named tokens
and add four files:
txtsrc
└─ /tokens
├─ border.js
├─ color.js
├─ space.js
└─ typography.js
The tokens for border, space, and typography will be the same no matter which theme is active. Enter the content for each of those files (using pixel units for sake of simplicity):
border.js
jsexport const borderRadiusSm = '4px'
export const borderRadius = '8px'
export const borderWidth = '1px'
export const borderStyle = 'solid'
space.js
jsexport const s1 = '8px'
export const s2 = '12px'
export const s3 = '16px'
export const s4 = '24px'
export const s5 = '40px'
typography.js
jsexport const fontFamily = 'sans-serif'
export const fontSizeText = '16px'
export const fontWeightRegular = 400
export const fontWeightBold = 700
export const lineHeightInput = 1.1
export const lineHeightMeta = 1.3
The color file is set up in the same way except that we have two sets of values: one for the dark theme and another for the light theme.
color.js
jsexport const darkWhite = '#222222'
export const darkGray1 = '#333333'
export const darkGray2 = '#5f5f5f'
export const darkGray3 = '#888888'
export const darkGray4 = '#e0e0e0'
export const darkGray5 = '#f5f5f5'
export const darkBlack = '#ffffff'
export const darkIndigo = '#3d4aff'
export const darkMagenta = '#b611a9'
export const lightWhite = '#ffffff'
export const lightGray1 = '#f5f5f5'
export const lightGray2 = '#e0e0e0'
export const lightGray3 = '#888888'
export const lightGray4 = '#5f5f5f'
export const lightGray5 = '#333333'
export const lightBlack = '#222222'
export const lightIndigo = '#3333ee'
export const lightMagenta = '#bb11bb'
export const trueWhite = '#ffffff'
Next we will collect all of these tokens into a single theme.js
file that we will pass to Emotion as the point of reference between our design tokens and the styled components.
Create a new directory in src
called styles
and add a new file theme.js
to this directory:
txtsrc
└─ /styles
└─ theme.js
Add the following content to theme.js
(and then we’ll walk through it):
jsimport * as color from '../tokens/color'
import * as border from '../tokens/border'
import * as space from '../tokens/space'
import * as typography from '../tokens/typography'
const themeDarkColor = {
white: color.darkWhite,
gray1: color.darkGray1,
gray2: color.darkGray2,
gray3: color.darkGray3,
gray4: color.darkGray4,
gray5: color.darkGray5,
black: color.darkBlack,
indigo: color.darkIndigo,
magenta: color.darkMagenta,
trueWhite: color.trueWhite
}
const themeLightColor = {
white: color.lightWhite,
gray1: color.lightGray1,
gray2: color.lightGray2,
gray3: color.lightGray3,
gray4: color.lightGray4,
gray5: color.lightGray5,
black: color.lightBlack,
indigo: color.lightIndigo,
magenta: color.lightMagenta,
trueWhite: color.trueWhite
}
const theme = (mode) => ({
color: (mode === 'dark') ? themeDarkColor : themeLightColor,
border: {...border},
mode: mode,
space: {...space},
typography: {...typography}
})
export default theme
At the top we import our four design token groups. This imports all of the tokens for each group and assigns them to a single object. For example, all of the space tokens are now stored in the space
object, if you need to access the s2
token it’s available via space.s2
. If we had written it as import * as myVeryNiceSpaceTokens from '../../tokens/space'
they would all be stored in an object called myVeryNiceSpaceTokens
.
After importing the tokens we split the color values into two objects: one for the dark theme colors and the other for the default (light) theme colors. The other token values remain the same no matter which theme is selected. (The trueWhite
is for the occasion where you need a color to NOT change based on the theme value. In our case we’ll use it to set the button label text color.)
At the end of the file we set up a function theme
which returns an object with all of our theme-specific token values. The theme function has a single parameter, mode
, which is used to set the appropriate colors based on context. If “dark” is passed as the value for mode, then the theme function sets the dark theme colors as the default values for the color property. Otherwise it sets the light theme colors as the default.
For border, space, and typography we just simply copy the values to object properties of the same name; and then we also send along the mode
value as a property in case we ever need it. At the end of the file we export the theme
function.
6 Install Storybook Dark Mode Addon
Let’s take a quick pause and summarize what we’ve done so far:
- We installed and configured Storybook for React.
- We created React components and styled them using Emotion’s
styled
method. - We set up a mini design system comprised of four groups of design tokens.
- We created a theme file to collect all of the design tokens and return the context-specific tokens in a single object based on the mode requested (i.e. if the tokens for “dark” are requested it sends the color values for the dark theme).
What’s left to do is:
- Install and configure the
storybook-dark-mode
addon. - Create a connection between the Storybook dark mode control and our React components.
- Update our React component’s styles to use the design tokens so our components update when the theme context is changed by Storybook.
—
In this section we will set up the addon and lay the groundwork for the next section where we will create a connection between the React components and the Storybook dark mode control.
To use storybook-dark-mode, we also need to install Storybook’s addon core module, @storybook/addons. Install them both with this command:
bashyarn add -D @storybook/addons storybook-dark-mode
Your package.json
file should look similar to this (as mentioned prior, your specific version numbers may be different):
json{
"name": "storybook-react-with-dark-mode",
"version": "0.1.0",
"scripts": {
"storybook": "start-storybook -p 9001"
},
"devDependencies": {
"@babel/core": "^7.11.1",
"@storybook/addons": "^6.0.4",
"@storybook/react": "^6.0.1",
"babel-loader": "^8.1.0",
"storybook-dark-mode": "^1.0.0"
},
"dependencies": {
"@emotion/core": "^10.0.34",
"@emotion/styled": "^10.0.27",
"emotion-theming": "^10.0.27",
"react": "^16.13.1",
"react-dom": "^16.13.1"
}
}
This adds the NPM modules to our project, but the dark mode addon is not activated until we register it with Storybook. To do this we are going to reference it from our .storybook/main.js
file by inserting a new object property addons
and activating the new addon by passing its register path as an item in the addons array.
Update .storybook/main.js
:
jsmodule.exports = {
addons: [
'storybook-dark-mode/register'
],
stories: ['../src/**/*.stories.[tj]s']
}
Now if you run Storybook (yarn storybook
) you should see a new moon icon in the toolbar above the story preview window. (If you already had Storybook running, you will need to restart it in this case.) If you click the icon the Storybook UI should toggle between dark mode and light mode.
However, our components remain the same because they don’t know that Storybook’s context is being changed. (Also because they still are being styled with static values, but we will fix this in the last section.) Let’s take a brief digression into how Storybook is setup which will help us better understand the next section as well.
Storybook is comprised of two separate applications — manager and preview — that communicate with one another via the Web API postMessage method. Manager is the outside frame of storybook and Preview is an iframe in the middle that displays the stories. See the image from Storybook’s website below.
The dark mode addon operates in the Manager application and thus by default only affects the UI of the manager space. We will have to write some extra code to send the toggle signal to the Preview application. To do this we will take advantage of a configuration file called preview.js
.
Storybook recognizes two files as special configuration files that correspond to the two different applications: manager.js
and preview.js
. When added to the main Storybook directory (.storybook
), the configuraton commands in these files are picked up automatically by Storybook. Generally speaking, manager.js is used to adjust Storybook’s UI, and preview.js is used to add extra context to components that are rendered in the preview space.
In the next section we’ll configure preview.js to get the dark mode context from the Manager application and pass it along to our components.
7 Connect the React Components to the Storybook Dark Mode Control
In the .storybook
directory, create a new file named preview.js
and for now just add a console message as its contents:
jsconsole.log('Here!')
If you run Storybook (restart it if it was already running) and inspect any of the stories and view the console you should see “Here!” logged when the page is refreshed. The preview.js file is being picked up by Storybook as expected.
In this preview.js file we will access the dark mode control signal via a hook provided by the addon called useDarkMode
. We will use this hook in combination with the theme
function created in Section 5 to send context-specific design tokens to our styled components with Emotion’s ThemeProvider
component.
However, in order to be able to use the React hook, we need a functional component to call it from. To do this we are going to use a Storybook concept called “decorators”. As the Storybook documentation puts it: “A decorator is a way to wrap a story in extra ‘rendering’ functionality.”
Decorators can be written on the individual story level, but we are going to write a global decorator in the preview.js file that will be used for all stories. In preview.js
remove the test console log and replace it with the contents below and then we’ll walk through it:
jsimport React from 'react'
import { useDarkMode } from 'storybook-dark-mode'
export const decorators = [
(Story) => {
console.log(useDarkMode())
return <Story />
}
]
The first thing we do is import React and then the useDarkMode
hook from storybook-dark-mode. Then we define a special variable called decorators
and export it. Storybook recognizes this decorators
variable and will run its contents as a parent component when any story is rendered. It is an array which means you can have multiple decorators on the global level, but in our case we only need the one.
The Story
parameter that is passed is the invidiual story that is rendered. Don’t be confused by the <Story />
component (perhaps it was just me!), it’s just the React syntax for rendering the function parameter as a component. It can be named anything, for example:
jsx(MySpecialStory) => {
console.log(useDarkMode())
return <MySpecialStory />
}
But I would keep it as Story
.
Finally, within the decorator we are logging the result of the useDarkMode()
hook. If you view the console for any story you should see true
logged when dark mode is toggled on and false
when it’s off. What’s important about this is the toggle action is occuring in the manager app and we are receiving the signal in the preview app. So we have now connected the dark mode toggle between the two.
If you recall in Section 5, we set up a theme
file to collect our design tokens and return the tokens for either light or dark mode based on a single function parameter we called “mode”. If “mode” is equal to dark
it sends along the tokens to style the components for the dark mode, otherwise it sends the tokens for light mode.
Since we now have a boolean for whether or not we’re in dark mode we can use that to set the parameter to pass to our theme
file to return the context-specific tokens. Like this:
jsconst mode = useDarkMode() ? 'dark' : 'light'
const thisTheme = theme(mode)
Then the way we “connect” our design tokens to our components is by using Emotion’s ThemeProvider which makes the theme values available in Emotion’s styled
function as part of the props
object. This is accomplished by importing the ThemeProvider
component, passing our theme style values via the component’s built-in theme
prop, and then setting it as the parent component of the Story
component. Emotion does the work from there.
Once this is in place, whenever we create a styled component with styled
we will have access to all of the design tokens in our theme object. (We will implement this in the next section.)
Update .storybook/preview.js
to import ThemeProvider from emotion-theming and the theme function from styles/theme.js, and then connect it all in the decorator:
jsimport React from 'react'
import { ThemeProvider } from 'emotion-theming'
import { useDarkMode } from 'storybook-dark-mode'
import theme from '../src/styles/theme'
export const decorators = [
(Story) => {
const mode = useDarkMode() ? 'dark' : 'light'
const thisTheme = theme(mode)
return (
<ThemeProvider theme={thisTheme}>
<Story />
</ThemeProvider>
)
}
]
We pass our mode
prop to the theme
function which returns either the light theme or dark theme tokens based on the value of mode
, and we save these to a new variable called thisTheme
. Then we assign this new variable to ThemeProvider
’s theme
prop and voilà our token values are now accessible from within Emotion’s styled
function.
If you view a story now and toggle dark mode on and off the component itself still will not change because we haven’t updated the hard-coded styles to use design tokens. We will do this next. But before we move to the next section I want to cover how to change the values of the Storybook dark mode theme and how to access the theme values outside of styled
.
Changing the Appearance of Storybook Dark Mode
In our case we are going to change the main background of the preview space and the toolbar above it to be a darker shade of black than the sidebar background. From what I understand, the storybook-dark-mode addon uses Storybook’s built-in dark theme for the display and simply provides the toggle functionality to switch it on and off.
To override any of the default values for Storybook’s dark theme we can use a built-in Storybook method similar to decorators called “parameters”. Parameters work the same way where you declare and export a variable called parameters
and Storybook recognizes this variable and merges the defined data in the parameters variable to the overall configuration.
The values of the dark theme are stored in a object that can be imported from @storybook/theming
(this directory was added automatically when we installed @storybook/react in Section 1). We import this because we must define every parameter when we add our custom values for the dark theme. We are only changing two values but we will need to copy the rest as part of the change.
Update .storybook/preview.js
:
jsimport React from 'react'
import { ThemeProvider } from 'emotion-theming'
import { themes } from '@storybook/theming'
import { useDarkMode } from 'storybook-dark-mode'
import theme from '../src/styles/theme'
export const parameters = {
darkMode: {
dark: {
...themes.dark, // copy existing values
appContentBg: '#202020', // override main story view frame
barBg: '#202020' // override top toolbar
}
}
}
export const decorators = [
(Story) => {
const mode = useDarkMode() ? 'dark' : 'light'
const thisTheme = theme(mode)
return (
<ThemeProvider theme={thisTheme}>
<Story />
</ThemeProvider>
)
}
]
Now if you view Storybook in dark mode, the main preview area and toolbar above it should be darker than the rest of the UI.
Access Theme Values Outside of styled
There may be cases where you need to access the theme values outside of the styled
context — to pass a theme-specific color value as a prop to another component for example. Emotion provides a hook called useTheme
that can access the theme values in ThemeProvider outside of the styled
function.
Update the Button.stories.js
with the following code to see an example:
jsimport React from 'react'
import { useTheme } from 'emotion-theming'
import Button from './'
export default {
title: 'Button'
}
export const Basic = () => {
const theme = useTheme()
console.log('theme values in story:')
console.log(theme)
return (
<Button>Submit</Button>
)
}
export const Disabled = () => (
<Button disabled>Submit</Button>
)
If you view the Button/Basic story and inspect the console you should see the current theme values logged:
Should they ever be needed, this is a way to access the theme values outside of a styled component.
8 Update Component Styles with Design Tokens
We have now configured Storybook and synced the dark mode toggle to our React components. The last step in our process is to replace the hard-coded style values in our component’s CSS with their design token counterparts. In doing this, their styles will be dynamic and will change based on the theme context.
In the styled components, thanks to the global decorator that wraps the component with Emotion’s ThemeProvider, our theme values are now included in a props
object which can be called at any point within styled
. You could opt to destructure the theme
object from props like so:
jsconst Example = styled.h1`
`
But for simplicity’s sake we’ll just call the entire props object and then reference theme
from there:
jsconst Example = styled.h1`
`
Let’s start with Button.component.js
, update the content to match:
jsimport React from 'react'
import styled from '@emotion/styled'
import * as border from '../../tokens/border'
import * as space from '../../tokens/space'
import * as typography from '../../tokens/typography'
const StyledButton = styled.button`
`
const Button = ({ children, disabled, ...rest }) => (
<StyledButton
{...{ children, disabled }}
{...rest}
/>
)
export default Button
Save this file and view the Button/Basic story in Storybook and toggle dark mode on and off:
It looks nearly the same as before, but if you inspect the button element in dark mode you should see the button background color as #3d4aff
rather than the default light mode value of #3333ee
. This is how we know that our dynamic theme setup is working.
Let’s replace the hard-coded styles in the rest of the *.component files:
Card.component.js
jsimport React from 'react'
import styled from '@emotion/styled'
import * as border from '../../tokens/border'
import * as space from '../../tokens/space'
import * as typography from '../../tokens/typography'
const StyledCard = styled.div`
`
const Card = ({ children }) => (
<StyledCard {...{ children }} />
)
export default Card
Input.component.js
jsimport React from 'react'
import styled from '@emotion/styled'
import * as border from '../../tokens/border'
import * as space from '../../tokens/space'
import * as typography from '../../tokens/typography'
const StyledInput = styled.input`
`
const Input = ({ children, disabled, ...rest }) => (
<StyledInput
{...{ disabled }}
{...rest}
/>
)
export default Input
Label.component.js
jsimport React from 'react'
import styled from '@emotion/styled'
import * as border from '../../tokens/border'
import * as space from '../../tokens/space'
import * as typography from '../../tokens/typography'
const StyledLabel = styled.label`
`
const Label = ({ children, disabled, ...rest }) => (
<StyledLabel
{...{ children, disabled }}
{...rest}
/>
)
export default Label
Now, if you navigate through the stories in Storybook and toggle dark mode on and off you should see everything switching in tandem:
And that’s it! Again, all of this code is on GitHub — if you have any questions or run into trouble please feel free to create an issue there and I’ll see what I can do to help.
Bonus: Adding Webfonts to Storybook
This section explains two methods of how to load custom webfonts into Storybook. Neither are very complicated, but it’s helpful to see them outlined. Storybook’s documentation on the subject is at the bottom of the Setup Storybook page in the “Render component styles” section under the “Add external CSS or fonts in the ‘head’” subsection and the “Load assets and resources” section.
preview-head.html
Storybook provides a special file called preview-head.html
for loading custom styles, scripts, etc. into the preview app (that is, the iFrame where your component is rendered). The only requirement is the file must be located in the main Storybook config directory. Anything you place in this file will be loaded into the <head></head>
of the preview app, while the main Storybook UI, the manager app, remains unaffected.
If you are loading webfonts from a 3rd-party provider like Google or a content delivery network (CDN), this method is an easy way to make them available to the components. In our project it would look like this:
txt.storybook
├─ main.js
├─ preview-head.html
└─ preview.js
And then in preview-head.html
you could add this for example (note that you don’t need the parent <head>
tags, just the content tags):
html<link href="https://fonts.googleapis.com/css2?family=Grenze&display=swap" rel="stylesheet">
And then the Regular, Italic and Bold weights of the Grenze type family would be available to use. But what if you had custom webfont files that weren’t hosted on an external service?
-s flag
Let’s say you have a set of webfonts that you want to include as part of the component repository and you place them in their own directory named webfonts
which you then place in src
:
txtsrc
├─ /components
├─ /styles
├─ /tokens
└─ /webfonts
└─ Grenze-Regular.woff2
Then in preview-head.html
we could add a <style>
tag and add a @font-face rule (really you’d probably want to create a new .css file, add the style there and then link to the .css file in preview-head.html, but I’m doing it this way to keep things succinct):
html<style>
/* Regular */
@font-face {
font-family: "Grenze-Regular";
src: url("../src/webfonts/Grenze-Regular.woff2") format("woff2");
font-style: normal;
font-weight: 400;
}
</style>
However, this won’t work because that relative path has no meaning when it’s injected into our Storybook component iFrame. To fix this you can assign a directory (or directories) as the place for Storybook to search for static content with the -s
flag in the start-storybook
command.
In our case, we would need to do two things. First, edit the command in package.json
to enable the webfonts directory as a static path (where ./src/webfonts is relative from the location of package.json, the project’s root directory):
json"scripts": {
"storybook": "start-storybook -p 9001 -s ./src/webfonts"
},
And then update the path to the webfont in preview-head.html
accordingly:
html<style>
/* Regular */
@font-face {
font-family: "Grenze-Regular";
src: url("/Grenze-Regular.woff2") format("woff2");
font-style: normal;
font-weight: 400;
}
</style>
Then you would be able to use Grenze-Regular as a font-family reference in your components. This is a very specific setup, you could have other patterns like a top-level public
directory and place the webfonts there for example. Or you could specify multiple paths separated by commas like so: -s ./public,./src/webfonts
.
Other Tutorials
- Docker + WordPress Setup
- → Simple Storybook React Setup with Light and Dark Mode
- Create Your Own Dynamic Gutenberg Block for Wordpress, Part 1
- Create Your Own Dynamic Gutenberg Block for Wordpress, Part 2
- Installing Composer
- How to Create a Blog with the Airtable API & Next.js