Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Themes #5889

Closed
seanmcintyre opened this issue Mar 5, 2019 · 25 comments
Closed

Dynamic Themes #5889

seanmcintyre opened this issue Mar 5, 2019 · 25 comments

Comments

@seanmcintyre
Copy link

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:

  • OS: macOS
  • Device: all
  • Browser: all
  • Framework: React
  • Addons: @storybook/theming, @storybook/addon-knobs
  • Version: 5.0.0
@shilman
Copy link
Member

shilman commented Mar 6, 2019

Take a look at this PR to see how @hipstersmoothie did it? #5841

@seanmcintyre
Copy link
Author

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?

@seanmcintyre
Copy link
Author

seanmcintyre commented Mar 6, 2019

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>
    );
  },
});

@whengely
Copy link

whengely commented Mar 7, 2019

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.

@seanmcintyre
Copy link
Author

Where did you read this is not possible until 5.1?

@seanmcintyre
Copy link
Author

seanmcintyre commented Mar 7, 2019

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>
    );
  },
});

iris-sb-themed

@seanmcintyre
Copy link
Author

Update update: It doesn't persist when switching stories. Still working on that.

@hipstersmoothie
Copy link
Contributor

I think @ndelangen has a plan on exposing an API to getOptions from an addon

@hipstersmoothie
Copy link
Contributor

@seanmcintyre you might also be interested in hipstersmoothie/storybook-dark-mode#1

@seanmcintyre
Copy link
Author

I've tried something similar. It seems hard to find documentation of the 5.0 API 😅

@seanmcintyre
Copy link
Author

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.

@stale stale bot added the inactive label Mar 31, 2019
@stramel
Copy link

stramel commented Apr 19, 2019

Would love official support for this! Thanks @seanmcintyre for the workaround

@stale stale bot removed the inactive label Apr 19, 2019
@domyen
Copy link
Member

domyen commented Apr 19, 2019

This makes sense as an addon to me. It's amazing! Keep it up @seanmcintyre 👏

@stale stale bot added the inactive label May 10, 2019
@nfarina
Copy link

nfarina commented May 14, 2019

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.

@stale stale bot removed the inactive label May 14, 2019
@ndelangen
Copy link
Member

That's really cool @nfarina

@faizalMo
Copy link

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.

Thanks for sharing that @seanmcintyre. How far away is the addon?

@Yankovsky
Copy link
Contributor

Yankovsky commented Jun 4, 2019

@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
https://camo.githubusercontent.com/ceb01675678a451312afac04509d8883b45d95d9/687474703a2f2f732e63737373722e72752f5530324432343854362f323031392d30362d30342d303730392d376f35716868707831722e706e67
Theme is just a class added to document.body.

@storybookjs storybookjs deleted a comment from stale bot Jun 4, 2019
@storybookjs storybookjs deleted a comment from stale bot Jun 4, 2019
@ndelangen
Copy link
Member

Adding fully dynamic theming in #6806

@ndelangen ndelangen self-assigned this Jun 4, 2019
@ndelangen ndelangen added this to the 6.0.0 milestone Jun 4, 2019
@faizalMo
Copy link

faizalMo commented Jun 5, 2019

@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.

@stale
Copy link

stale bot commented Jun 26, 2019

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!

@stale stale bot added the inactive label Jun 26, 2019
@stale
Copy link

stale bot commented Jul 26, 2019

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!

@stale stale bot closed this as completed Jul 26, 2019
@jraeruhl
Copy link

jraeruhl commented Nov 6, 2019

@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.

@Yankovsky
Copy link
Contributor

@jraeruhl https://github.com/tonai/storybook-addon-themes
I just change css class on body.

@seanmcintyre
Copy link
Author

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

#8518

@supersnager
Copy link

Note:
"storiesConfigured" event, no longer exists in version 6.x.
I think that "storyRendered" event can be used instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests