Unsure if this is a bug report or a feature request 馃槄
The current docs state:
Changing theme at runtime is supported!
I am specifically trying to do this in conjunction with styled-components <ThemeProvider />
and @storybook/addon-knobs
. However, I have not yet been able to figure out how to do this in even a simple manner. My ultimate goal is to change both the Storybook theme and the theme of my components in tandem.
My first attempt was as follows:
config.tsx
...
import { ThemeProvider } from 'styled-components';
import { select } from '@storybook/addon-knobs';
...
// create({...}) themes
import * as themes from './theme';
const StyledTheme = {
light: {
test: 'blue',
color: '#FFF',
name: 'light',
},
dark: {
test: 'red',
color: '#000',
name: 'dark',
},
};
const setTheme = theme =>
addParameters({
options: {
theme: themes[theme.name],
},
});
setTheme(StyledTheme.light);
addDecorator(story => {
const theme = select('theme', StyledTheme, 'light');
return (
<ThemeProvider theme={theme}>{story({ theme })}</ThemeProvider>
);
});
...
System:
@storybook/theming
, @storybook/addon-knobs
Take a look at this PR to see how @hipstersmoothie did it? https://github.com/storybooks/storybook/pull/5841
My use case is fairly different. I elected to go the route of writing an addon, but currently struggling with getting the theme data to pass to and re-render the iframe with the story wrapped in a ThemeProvider
. I haven't found documentation for doing something like this.
So far I've figured out that I can grab the current theme information off the context in a decorator like so:
export const withTheme = (story, context) => {
const theme = context.parameters.options.theme.base;
return <ThemeProvider theme={theme}>story()</ThemeProvider>;
};
This doesn't seem to update when the theme is changed. Is there an event I can hook into from an addDecorator()
fn to re-render the story and persist the theme state across story changes?
I cannot figure out how to retrieve what I set via api.setOptions
in the decorator:
import React from 'react';
import { useState } from 'react';
import { themes } from '../../themes';
import {
IconButton,
WithTooltip,
TooltipLinkList,
} from '@storybook/components';
import addons, { types, makeDecorator } from '@storybook/addons';
import { FORCE_RE_RENDER } from '@storybook/core-events';
import styled, { ThemeProvider } from 'styled-components';
const ThemeIcon = styled.span`
height: 1rem;
width: 1rem;
display: block;
background: red;
`;
addons.register('storybook/theme-switcher', api => {
addons.add('storybook/theme-switcher', {
title: 'theme-switcher',
type: types.TOOL,
match: ({ viewMode }) => viewMode === 'story',
render: () => <ThemeSwitcher api={api} />,
});
});
export const ThemeSwitcher = ({ api }) => {
const [activeTheme, setTheme] = useState('light');
const [expanded, setExpanded] = useState(false);
const themeList = ['dark', 'light', 'a11y', 'vaporware'].map(i => ({
id: i,
title: i,
onClick: () => {
setTheme(i);
api.setOptions({
i,
theme: themes[i] || themes.light,
});
addons.getChannel().emit(FORCE_RE_RENDER);
},
right: <ThemeIcon />,
}));
return (
<WithTooltip
placement="top"
trigger="click"
tooltipShown={expanded}
onVisibilityChange={s => setExpanded(s)}
tooltip={<TooltipLinkList links={themeList} />}
closeOnClick
>
<IconButton key="theme-switcher">{activeTheme}</IconButton>
</WithTooltip>
);
};
export const withTheme = makeDecorator({
name: 'withTheme',
parameterName: 'theme',
skipIfNoParametersOrOptions: false,
allowDeprecatedUsage: false,
wrapper: (getStory, context) => {
const theme = context.parameters.options.theme;
console.log('context: ', context);
return (
<ThemeProvider theme={theme}>{getStory(context)}</ThemeProvider>
);
},
});
I've been following this because I'm very interested in it. When I read the linked issue, I kind of read it as not possible until 5.1 and then by means of an addon. I wonder if the docs got a bit ahead.
Where did you read this is not possible until 5.1?
Update: It's kind of a hack, but I pulled it off with localStorage
.
const themeList = ['dark', 'light', 'a11y', 'vaporware'].map(i => ({
id: i,
title: i,
onClick: () => {
setTheme(i);
api.setOptions({
i,
theme: themes[i] || themes.light,
});
window.localStorage.setItem('iris-sb-theme', i);
addons.getChannel().emit(FORCE_RE_RENDER);
},
right: <ThemeIcon />,
}));
export const withTheme = makeDecorator({
name: 'withTheme',
parameterName: 'theme',
skipIfNoParametersOrOptions: false,
allowDeprecatedUsage: false,
wrapper: (getStory, context) => {
const savedTheme = window.localStorage.getItem('iris-sb-theme');
const theme =
typeof themes[savedTheme] === 'object'
? themes[savedTheme]
: themes.light;
return (
<ThemeProvider theme={theme}>{getStory(context)}</ThemeProvider>
);
},
});
Update update: It doesn't persist when switching stories. Still working on that.
I think @ndelangen has a plan on exposing an API to getOptions from an addon
@seanmcintyre you might also be interested in https://github.com/hipstersmoothie/storybook-dark-mode/issues/1
I've tried something similar. It seems hard to find documentation of the 5.0 API 馃槄
Figured it out. This _definitely_ still feels like a hack though.
function bindThemeOverride(api) {
const channel = api.getChannel();
channel.on('storiesConfigured', () => {
setLocalTheme({ api });
});
channel.on('storyChanged', () => {
setLocalTheme({ api });
});
}
Full code:
import React, { useEffect } from 'react';
import { useState } from 'react';
import { themes } from '../../themes';
import {
IconButton,
WithTooltip,
TooltipLinkList,
} from '@storybook/components';
import addons, { types, makeDecorator } from '@storybook/addons';
import { FORCE_RE_RENDER } from '@storybook/core-events';
import styled, { ThemeProvider } from 'styled-components';
addons.register('storybook/theme-switcher', api => {
addons.add('storybook/theme-switcher', {
title: 'theme-switcher',
type: types.TOOL,
match: ({ viewMode }) => viewMode === 'story',
render: () => <ThemeSwitcher api={api} />,
});
});
export const withTheme = makeDecorator({
name: 'withTheme',
parameterName: 'theme',
skipIfNoParametersOrOptions: false,
allowDeprecatedUsage: false,
wrapper: (getStory, context) => (
<ThemeProvider theme={getLocalTheme()[1]}>
{getStory(context)}
</ThemeProvider>
),
});
export const ThemeSwitcher = ({ api }) => {
const [activeTheme, setTheme] = useState(getLocalTheme()[0]);
const [expanded, setExpanded] = useState(false);
useEffect(() => bindThemeOverride(api), []);
const themeList = ['dark', 'light', 'a11y', 'vaporware'].map(i => ({
id: i,
title: i,
onClick: () => {
setTheme(i);
setLocalTheme({ api, theme: i, rerender: true });
},
right: <ThemeIcon />,
}));
return (
<WithTooltip
placement="top"
trigger="click"
tooltipShown={expanded}
onVisibilityChange={s => setExpanded(s)}
tooltip={<TooltipLinkList links={themeList} />}
closeOnClick
>
<IconButton key="theme-switcher">{activeTheme}</IconButton>
</WithTooltip>
);
};
const ThemeIcon = styled.span`
height: 1rem;
width: 1rem;
display: block;
background: red;
`;
function bindThemeOverride(api) {
const channel = api.getChannel();
channel.on('storiesConfigured', () => {
setLocalTheme({ api });
});
channel.on('storyChanged', () => {
setLocalTheme({ api });
});
}
function setLocalTheme({
api,
theme = getLocalTheme()[0],
rerender = false,
}) {
window.localStorage.setItem('iris-sb-theme', theme);
api.setOptions({
theme: getLocalTheme()[1],
});
if (rerender) {
addons.getChannel().emit(FORCE_RE_RENDER);
}
}
function getLocalTheme() {
const savedTheme = window.localStorage.getItem('iris-sb-theme');
const theme =
typeof themes[savedTheme] === 'object'
? themes[savedTheme]
: themes.light;
return [savedTheme, theme];
}
I'm going to clean this up and release an addon that takes an array of themes from a .storybook/themes.ts
(or js), adds them to the current dark/light default themes, allows dark/light to be overridden by your own variations, and makes the use of styled-components ThemeProvider
optional.
Would love official support for this! Thanks @seanmcintyre for the workaround
This makes sense as an addon to me. It's amazing! Keep it up @seanmcintyre 馃憦
So I personally only want Storybook's theme to change along with my macOS system theme (for when I'm working at night). My app itself already uses the prefers-dark-mode
media query and I wanted Storybook's UI to do the same. I took @seanmcintyre's (excellent) sample code and made this very quick and simple alternative:
https://gist.github.com/nfarina/fb708f66858d2d3317877ab8adf8d926
Simply add the file to your project and import it in your addons.js
.
That's really cool @nfarina
Figured it out. This _definitely_ still feels like a hack though.
function bindThemeOverride(api) { const channel = api.getChannel(); channel.on('storiesConfigured', () => { setLocalTheme({ api }); }); channel.on('storyChanged', () => { setLocalTheme({ api }); }); }
Full code:
import React, { useEffect } from 'react'; import { useState } from 'react'; import { themes } from '../../themes'; import { IconButton, WithTooltip, TooltipLinkList, } from '@storybook/components'; import addons, { types, makeDecorator } from '@storybook/addons'; import { FORCE_RE_RENDER } from '@storybook/core-events'; import styled, { ThemeProvider } from 'styled-components'; addons.register('storybook/theme-switcher', api => { addons.add('storybook/theme-switcher', { title: 'theme-switcher', type: types.TOOL, match: ({ viewMode }) => viewMode === 'story', render: () => <ThemeSwitcher api={api} />, }); }); export const withTheme = makeDecorator({ name: 'withTheme', parameterName: 'theme', skipIfNoParametersOrOptions: false, allowDeprecatedUsage: false, wrapper: (getStory, context) => ( <ThemeProvider theme={getLocalTheme()[1]}> {getStory(context)} </ThemeProvider> ), }); export const ThemeSwitcher = ({ api }) => { const [activeTheme, setTheme] = useState(getLocalTheme()[0]); const [expanded, setExpanded] = useState(false); useEffect(() => bindThemeOverride(api), []); const themeList = ['dark', 'light', 'a11y', 'vaporware'].map(i => ({ id: i, title: i, onClick: () => { setTheme(i); setLocalTheme({ api, theme: i, rerender: true }); }, right: <ThemeIcon />, })); return ( <WithTooltip placement="top" trigger="click" tooltipShown={expanded} onVisibilityChange={s => setExpanded(s)} tooltip={<TooltipLinkList links={themeList} />} closeOnClick > <IconButton key="theme-switcher">{activeTheme}</IconButton> </WithTooltip> ); }; const ThemeIcon = styled.span` height: 1rem; width: 1rem; display: block; background: red; `; function bindThemeOverride(api) { const channel = api.getChannel(); channel.on('storiesConfigured', () => { setLocalTheme({ api }); }); channel.on('storyChanged', () => { setLocalTheme({ api }); }); } function setLocalTheme({ api, theme = getLocalTheme()[0], rerender = false, }) { window.localStorage.setItem('iris-sb-theme', theme); api.setOptions({ theme: getLocalTheme()[1], }); if (rerender) { addons.getChannel().emit(FORCE_RE_RENDER); } } function getLocalTheme() { const savedTheme = window.localStorage.getItem('iris-sb-theme'); const theme = typeof themes[savedTheme] === 'object' ? themes[savedTheme] : themes.light; return [savedTheme, theme]; }
I'm going to clean this up and release an addon that takes an array of themes from a
.storybook/themes.ts
(or js), adds them to the current dark/light default themes, allows dark/light to be overridden by your own variations, and makes the use of styled-componentsThemeProvider
optional.
Thanks for sharing that @seanmcintyre. How far away is the addon?
@seanmcintyre @faizalMo does this addon works for you? https://github.com/tonai/storybook-addon-themes/tree/master/src
It allows you to select theme in top menu
Theme is just a class added to document.body.
Adding fully dynamic theming in #6806
@Yankovsky my use case was slightly different. I wanted a way to easily switch between override CSS files so that our WebComponents showcased in Storybook can be displayed in different styles/themes. I achieved this by adding the CSS using preview-head.html and then switching it with custom storybook addon.
Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 30 days. Thanks!
Hey there, it's me again! I am going close this issue to help our maintainers focus on the current development roadmap instead. If the issue mentioned is still a concern, please open a new ticket and mention this old one. Cheers and thanks for using Storybook!
@Yankovsky my use case was slightly different. I wanted a way to easily switch between override CSS files so that our WebComponents showcased in Storybook can be displayed in different styles/themes. I achieved this by adding the CSS using preview-head.html and then switching it with custom storybook addon.
Which addon did you use? I am trying to find a solution for swapping out my custom prop and scss imports based on theme for our components (not the Storybook UI theme), ideally on-the-fly.
@jraeruhl https://github.com/tonai/storybook-addon-themes
I just change css class on body.
This is a little bit of a hack, because I have not yet been able to correctly use useAddonState
, but @nox/addon-themes
will allow you to register a light and dark theme and toggle between them:
https://github.com/seanmcintyre/addon-themes
https://www.npmjs.com/package/@nox/addon-themes
Note:
"storiesConfigured" event, no longer exists in version 6.x.
I think that "storyRendered" event can be used instead.
Most helpful comment
Figured it out. This _definitely_ still feels like a hack though.
Full code:
I'm going to clean this up and release an addon that takes an array of themes from a
.storybook/themes.ts
(or js), adds them to the current dark/light default themes, allows dark/light to be overridden by your own variations, and makes the use of styled-componentsThemeProvider
optional.