storybook

How to customize both dark and light themes in Storybook 6.0


I am making a PWA using light and dark themes, and I want to create my Storybook light and dark themes to reflect those.

So I created a function that will return a new Storybook theme object to me if I pass it a Material UI Theme and a base name.

But how do I pass these 2 objects to Storybook as themes?

I have found out that I am supposed to go to manager.js and add the following code

import theme from '../src/theme/theme'
import createThemeFromMUITheme from './create-theme-from-mui-theme'
import addons from '@storybook/addons'

addons.setConfig({
  theme: createThemeFromMUITheme('light', theme.light),
})

But how do I set theme for both light and dark?

I have tried

import theme from '../src/theme/theme'
import createThemeFromMUITheme from './create-theme-from-mui-theme'
import addons from '@storybook/addons'

addons.setConfig({
  theme: {
    light: createThemeFromMUITheme('light', theme.light),
    dark: createThemeFromMUITheme('dark', theme.dark)
  },
})

But that makes the storybook show nothing (it does not fail however)

Help please :-)

EDIT: I have also tried the following

import theme from '../src/theme/theme'
import createThemeFromMUITheme from './create-theme-from-mui-theme'
import addons from '@storybook/addons'

addons.setConfig({
  theme: createThemeFromMUITheme('light', theme.light),
})

addons.setConfig({
  theme: createThemeFromMUITheme('dark', theme.dark),
})

EDIT #2: The returned theme config object from createThemeFromMUITheme is valid BTW

And if anyone should want the function I made to convert a MUI theme object into a SB theme object - then this is it...

(I did not mess with form colors yet...)

import { create } from '@storybook/theming/create'

const createThemeFromMUITheme = (name, theme) => {
  return create({
    base: name,

    colorPrimary: theme.palette.primary.main,
    colorSecondary: theme.palette.secondary.main,

    // UI
    appBg: theme.palette.background.default,
    appContentBg: theme.palette.background.paper,
    appBorderColor: theme.palette.background.paper,
    appBorderRadius: theme.shape.borderRadius,

    // Typography
    fontBase: theme.typography.fontFamily,
    fontCode: 'monospace',

    // Text colors
    textColor: theme.palette.text.primary,
    textInverseColor: theme.palette.text.secondary,

    // Toolbar default and active colors
    barTextColor: theme.palette.text.primary,
    barSelectedColor: theme.palette.text.secondary,
    barBg: theme.palette.background.default,

    brandTitle: 'Add your brand title here',
    brandUrl: 'https://yourbrandurl.com',
    brandImage: 'https://placehold.it/350x150',
  })
}

export default createThemeFromMUITheme


Solution

  • Well - the solution was staring me right in the eyes, but as usual I had not read the documentation enough.

    I did have storybook-dark-theme installed, and my idea was to have MUI fully integrated so that the light theme will show my application light theme, and the dark theme will show my dark theme - and the components likewise.

    This way the light/dark theme switcher becomes fully global.

    It does however take a little more than what you wrote.

    And here is what I was aiming at

    light dark

    And you can download all of it from here Link to my github with the solution

    When storybook renders first time it will use the default theme, and as such you will get some flickering before the light/dark themes you have been overriding with kicks in.

    First click will make it remain light theme, but now it will use YOUR light theme.

    The solution to this is to initialize with the light theme in manager.ts like so

    import { addons } from '@storybook/addons'
    import { theme as appTheme } from '../src/theme/theme'
    import { createThemeFromMuiTheme } from './utils/create-theme-from-mui-theme'
    
    addons.setConfig({
        theme: createThemeFromMuiTheme({
            theme: appTheme.light,
            options: {
                base: 'light',
                brandTitle: 'Storybook with MUI',
                brandUrl: 'https://www.github.com/IgorSzyporyn/storybook-with-mui'
            },
        })
    })
    

    And then in preview.ts you need to then set the light/dark overrides like so

    import React from 'react'
    import { addDecorator } from '@storybook/react'
    import { theme as appTheme } from '../src/theme/theme'
    import { WithMuiTheme } from './components/WithMuiTheme'
    import { createThemeFromMuiTheme } from './utils/create-theme-from-mui-theme'
    
    addDecorator((story) => <WithMuiTheme>{story()}</WithMuiTheme>)
    
    export const parameters = {
        exportedParameter: 'exportedParameter',
        darkMode: {
            current: 'light',
            light: createThemeFromMuiTheme({ theme: appTheme.light, asStorybookTheme: false }),
            dark: createThemeFromMuiTheme({ theme: appTheme.dark, asStorybookTheme: false })
        },
    }
    

    And have your decorator wrapper made like this ./components/WithMuiThemeProps

    import React from 'react'
    import { MuiThemeProvider, CssBaseline, StylesProvider } from '@material-ui/core'
    import { useThemeType } from '../hooks/UseThemeType'
    import { theme } from '../../src/theme/theme'
    
    type WithMuiThemeProps = {
        children: React.ReactNode
    }
    
    export const WithMuiTheme = ({ children }: WithMuiThemeProps) => {
        const themeType = useThemeType()
    
        return (
            <MuiThemeProvider theme={theme[themeType]}>
                <CssBaseline />
                <StylesProvider injectFirst>{children}</StylesProvider>
            </MuiThemeProvider>
        )
    }
    

    And here is the hook that makes the update happen ./hooks/UseThemeType.ts

    import React from 'react'
    import addons from '@storybook/addons'
    
    const channel = addons.getChannel()
    
    export const useThemeType = () => {
        const [isDark, setDark] = React.useState(false)
    
        React.useEffect(() => {
            channel.on('DARK_MODE', setDark)
            return () => channel.off('DARK_MODE', setDark)
        }, [channel, setDark])
    
        const paletteType = isDark ? 'dark' : 'light'
    
        return paletteType
    }
    

    And finally here is ./utils/create-theme-from-mui-theme.ts

    import { create } from '@storybook/theming/create'
    import { Theme } from '@material-ui/core'
    
    type CreateThemFromMuiTheme = {
        theme: Theme
        options?: Object
        asStorybookTheme?: boolean
    }
    
    export const createThemeFromMuiTheme = ({
        theme,
        options = {},
        asStorybookTheme = true,
    }: CreateThemFromMuiTheme) => {
        const themeValue = {
            colorPrimary: theme.palette.primary.main,
            colorSecondary: theme.palette.secondary.main,
    
            // UI
            appBg: theme.palette.background.paper,
            appContentBg: theme.palette.background.default,
            appBorderColor: theme.palette.background.paper,
            appBorderRadius: theme.shape.borderRadius,
    
            // Typography
            fontBase: theme.typography.fontFamily,
            fontCode: 'monospace',
    
            // Text colors
            textColor: theme.palette.text.primary,
            textInverseColor: theme.palette.text.secondary,
    
            // Toolbar default and active colors
            barTextColor: theme.palette.text.secondary,
            barSelectedColor: theme.palette.secondary.main,
            barBg: theme.palette.background.default,
    
            // Form color
            inputBg: 'transparent',
            inputBorder: 'silver',
            inputTextColor: theme.palette.text.primary,
            inputBorderRadius: theme.shape.borderRadius,
    
            ...options,
        }
    
        return asStorybookTheme ? create(themeValue) : themeValue
    }
    

    And now you will be able to use the theme fully integrated into Storybook - when you switch storybook theme then Storybook itself will change into the theme AND the components you use will change as well.