storybook-dark-mode icon indicating copy to clipboard operation
storybook-dark-mode copied to clipboard

Storybook 6 docs theming

Open stuarthendren opened this issue 4 years ago • 36 comments

Can this be made to also configure the storybook docs theme which is supplied separately in v6.0?

stuarthendren avatar Oct 26 '20 17:10 stuarthendren

It could. The last time I looked that wasn't configurable. Would happily accept a PR for this :D

hipstersmoothie avatar Oct 26 '20 21:10 hipstersmoothie

I had a go at this but I can't see how to make it work. I can store the relevant docs theme in the globals but I can't see how to provide the theme to the docs. The DocContainer gets the values from the parameters but I don't see any api to set the parameters. I tried using a decorator and setting the parameter value but these seem to only be applied to the stories within the docs - which already worked by the existing mechanism.

stuarthendren avatar Oct 27 '20 16:10 stuarthendren

I would love to see this be possible somehow as well. I'm running into this issue as well. I may have a look.

jadenlemmon avatar Nov 03 '20 19:11 jadenlemmon

In preview.js you can supply a custom container and override the styles using the sbdocs css class(es). This could be a route to making this work:

// preview.js
export const parameters = {
  docs: {
    container: DocsContainerTheme,
  },
}

Then through some styling solution manipulate the theme. As a proof of concept, the following worked to switch the background and color, but would be better to use a theme parameter rather than reconstruct the whole css here:

// DocContainerTheme
import React from 'react'
import { DocsContainer } from '@storybook/addon-docs/blocks'
import { useDarkMode } from 'storybook-dark-mode'
import { makeStyles } from '../../src'

const useStyles = makeStyles({
  override: {
    '& .sbdocs': {
      background: ({ dark }) => (dark ? 'black' : 'white'),
      color: ({ dark }) => (dark ? 'white' : 'black'),
    },
  },
})

export const DocsContainerTheme = ({ children, context }) => {
  const dark = useDarkMode()
  const classes = useStyles({ dark })
  return (
    <div className={classes.override}>
      <DocsContainer context={context}>{children}</DocsContainer>
    </div>
  )
}

stuarthendren avatar Nov 06 '20 11:11 stuarthendren

I think official support for this is still waiting on https://github.com/storybookjs/storybook/issues/10523

Without a way to dynamically set params for the docs theme, this plugin can't do much. The approach above seems nice though in the interim

hipstersmoothie avatar Nov 19 '20 23:11 hipstersmoothie

Thanks for the sharing @stuarthendren!

I found a better workaround that will fully respect the used theming without hacking the css by overriding the context using spread operators.

First, create a doc container as the above example:

// .storybook/components/DocContainer.tsx
import React from 'react'
import { DocsContainer as BaseContainer } from '@storybook/addon-docs/blocks'
import { useDarkMode } from 'storybook-dark-mode'
import { themes } from '@storybook/theming';

export const DocsContainer = ({ children, context }) => {
  const dark = useDarkMode()

  return (
    <BaseContainer
      context={{
        ...context,
        parameters: {
          ...context.parameters,
          docs: {
            // This is where the magic happens.
            theme: dark ? themes.dark : themes.light
          },
        },
      }}
    >
      {children}
    </BaseContainer>
  );
}

Then on your preview.js config file:

// .storybook/preview.js
import { DocsContainer } from './components/DocContainer';

export const parameters = {
  docs: {
    container: DocsContainer,
  },
  // Rest of your configuration
};

And voilà! :tada:

UPDATE: Not working since Storybook 6.4. See https://github.com/hipstersmoothie/storybook-dark-mode/issues/127#issuecomment-983056445 or https://github.com/hipstersmoothie/storybook-dark-mode/issues/127#issuecomment-1070524402 for 6.4 compatible solution.

soullivaneuh avatar Mar 18 '21 15:03 soullivaneuh

return (
    <BaseContainer
      context={{
        ...context,
        parameters: {
          ...context.parameters,
          docs: {
            // This is where the magic happens.
            theme: dark ? themes.dark : themes.light
          },
        },
      }}
    >
      {children}
    </BaseContainer>
  );

This is life affirming. Surprised it's not in the docs as I don't consider it to be working properly if everything is dark and Docs are still blindingly white 😅

dominictobias avatar Apr 30 '21 17:04 dominictobias

I'm not using React (but Vue3) and didn't see anything on creating containers in Vue (I'm not even sure it's possible).

So I went with a very simple (but not reactive) solution, adding this in my preview.ts:

export const parameters = {
    /* ... */
    docs: {
        get theme() {
            let isDarkMode = parent.document.body.classList.contains("dark");
            return isDarkMode ? themes.dark : themes.light;
        }
    },
    /* ... */
};

It's not reactive, so you must reload the page. The check is kinda dirty/hacky. But it works for my case, so I thought I might share.

hugoattal avatar May 13 '21 17:05 hugoattal

the above solution is working for me for the theme but it stopped showing props table. any ideas?

Error: Args unsupported. See Args documentation for your framework. Read the docs

gsingh1370 avatar May 24 '21 19:05 gsingh1370

Yeah, same for me:

Screenshot 2021-05-28 at 10 57 44

@soullivaneuh @gsingh1370 @DominicTobias have you managed to make the props work?

gazpachu avatar May 28 '21 01:05 gazpachu

@gazpachu use this. Added docs from context

import React from 'react' import { DocsContainer as BaseContainer } from '@storybook/addon-docs/blocks' import { useDarkMode } from 'storybook-dark-mode' import { themes } from '@storybook/theming';

export const DocsContainer = ({ children, context }) => { const dark = useDarkMode()

return ( <BaseContainer context={{ ...context, parameters: { ...context.parameters, docs: { ...context.parameters.docs, // This is where the magic happens. theme: dark ? themes.dark : themes.light }, }, }} > {children} </BaseContainer> ); }

gsingh1370 avatar May 28 '21 10:05 gsingh1370

Thanks, at the end, with lodash/set, the props work fine:

import set from 'lodash/set';
import React, { ReactNode } from 'react';
import { useDarkMode } from 'storybook-dark-mode';

export const DocsContainer = ({
  children,
  context
}: {
  children: ReactNode;
  context: any;
}) => {
  const dark = useDarkMode();
  set(context, 'parameters.docs.theme', dark ? themes.dark : themes.light);
  return <BaseContainer context={context}>{children}</BaseContainer>;
};

gazpachu avatar May 28 '21 11:05 gazpachu

@gsingh1370 @gazpachu Not tested because I don't use props for my case, but as an alternative as the lodash example, you may use an additional spread operator:

diff --git a/.storybook/components/DocContainer.tsx b/.storybook/components/DocContainer.tsx
index a0fd4b7..d1d775d 100644
--- a/.storybook/components/DocContainer.tsx
+++ b/.storybook/components/DocContainer.tsx
@@ -18,6 +18,7 @@ export const DocsContainer: FC<DocsContainerProps> = ({ children, context }) =>
         parameters: {
           ...parameters,
           docs: {
+            ...parameters.docs,
             theme: dark ? themes.dark : themes.light,
           },
         },

This is surely why you don't have any prop, the docs part is completely overridden on my previous sample.

soullivaneuh avatar Oct 25 '21 14:10 soullivaneuh

Hmm I just upgraded from 6.4.0-beta.19 and the magic is no longer working, switching back to a "stuck light mode".

Does anyone got the same issue? Do you know if a workaround is possible?

soullivaneuh avatar Oct 25 '21 14:10 soullivaneuh

Storybook has released 6.4.

I haven't found a new way to set the theme dynamically. My very, very dirty hack with page refresh looks like this:

import React, { useEffect } from 'react';
import { addParameters } from '@storybook/react';
import { DocsContainer as BaseContainer } from '@storybook/addon-docs';
import { themes } from '@storybook/theming';
import { useDarkMode } from 'storybook-dark-mode';

const isInitialDark = JSON.parse(localStorage.getItem('sb-addon-themes-3') ?? '{}')?.current === 'dark';

addParameters({
  docs: {
    theme: isInitialDark ? themes.dark : themes.light,
  },
});

export const DocsContainer: typeof BaseContainer = (props) => {
  const isDark = useDarkMode();

  useEffect(
    () => {
      if (isInitialDark !== isDark) {
        window.location.reload();
      }
    },
    [isDark],
  )

  return <BaseContainer {...props} />;
};

I think this could potentially lead to endless page reloads. Use at your own risk.

dartess avatar Nov 29 '21 12:11 dartess

@dartess , @soullivaneuh after digging through the updates to DocsContainer in 6.4 , was able to get it working this way:

    <BaseContainer
      context={{
        ...context,
        storyById: (id) => {
          const storyContext = context.storyById(id);
          return {
            ...storyContext,
            parameters: {
              ...storyContext?.parameters,
              docs: {
                theme: dark ? themes.dark : themes.light,
              },
            },
          }
        },
      }}
    >

Not sure if this is the intended way, but it's working for me 😄

aaron-a-anderson avatar Nov 30 '21 21:11 aaron-a-anderson

Anyone have a similar issue with the controls addon? I've got everything dark but that portion of the UI. image

acc-nicholas avatar Dec 06 '21 19:12 acc-nicholas

I was able to get both the args table to work correctly with subcomponents and the controls section to show up in dark mode with this:

    <BaseContainer
      context={{
        ...context,
        storyById: (id) => {
          const storyContext = context.storyById(id)
          return {
            ...storyContext,
            parameters: {
              ...storyContext?.parameters,
              docs: {
                ...storyContext?.parameters?.docs,
                theme: dark ? darkTheme : themes.light,
              },
            },
          }
        },
      }}
    >

Kevin-S-Wallace avatar Dec 08 '21 17:12 Kevin-S-Wallace

Мне удалось заставить таблицу аргументов правильно работать с подкомпонентами и разделом управления, чтобы он отображался в темном режиме с помощью этого:

    <BaseContainer
      context={{
        ...context,
        storyById: (id) => {
          const storyContext = context.storyById(id)
          return {
            ...storyContext,
            parameters: {
              ...storyContext?.parameters,
              docs: {
                ...storyContext?.parameters?.docs,
                theme: dark ? darkTheme : themes.light,
              },
            },
          }
        },
      }}
    >

tell me where you used it

kidsamort avatar Jan 28 '22 08:01 kidsamort

For anyone reading this in 2022. This is full fix description:

  1. Create .storybook/DocsContainer.js
import React from "react";
import { DocsContainer as BaseContainer } from "@storybook/addon-docs/blocks";
import { useDarkMode } from "storybook-dark-mode";
import { themes } from "@storybook/theming";

export const DocsContainer = ({ children, context }) => {
  const dark = useDarkMode();

  return (
    <BaseContainer
      context={{
        ...context,
        storyById: (id) => {
          const storyContext = context.storyById(id);
          return {
            ...storyContext,
            parameters: {
              ...storyContext?.parameters,
              docs: {
                ...storyContext?.parameters?.docs,
                theme: dark ? themes.dark : themes.light,
              },
            },
          };
        },
      }}
    >
      {children}
    </BaseContainer>
  );
};

  1. Edit & tune for your needs: .storybook/preview.js
import React from "react";
import { useDarkMode } from "storybook-dark-mode";
import { themes } from "@storybook/theming";
import { darkTheme } from "@retrolove-games/ui-themes";
import { DocsContainer } from './DocsContainer';

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
  viewMode: "docs",
  docs: {
    // theme: themes.dark,
    container: DocsContainer,
  },
};

export const decorators = [
  (Story) => (
    <div className={useDarkMode() ? darkTheme.className : "light"}>
      <Story />
    </div>
  ),
];

Also published in my gist.

fedek6 avatar Mar 17 '22 08:03 fedek6

Same problem here, docs are not updating when changing theme. Would love a similar option for styling docs, just like { stylePreview: true }

For now I'm sticking with the hack by @hugoattal because I'm also using Vue for my UI library. I tried the custom DocsContainer solution, but I can't install React as a devDependency in Storybook 6.4.20, because npm is complaining about dependency conflicts... Let's hope this Storybook issue gets solved.

I'm not using React (but Vue3) and didn't see anything on creating containers in Vue (I'm not even sure it's possible).

So I went with a very simple (but not reactive) solution, adding this in my preview.ts:

export const parameters = {
    /* ... */
    docs: {
        get theme() {
            let isDarkMode = parent.document.body.classList.contains("dark");
            return isDarkMode ? themes.dark : themes.light;
        }
    },
    /* ... */
};

martijnhalekor avatar Apr 05 '22 11:04 martijnhalekor

Here is how I used @fedek6 DocsContainer with MUI5 theme:

import { createTheme, CssBaseline, ThemeProvider } from "@mui/material";
import { getDesignTokens } from "theme/theme";
import { useDarkMode } from "storybook-dark-mode";
import { themes } from "@storybook/theming";
import { DocsContainer } from "./DocsContainer";

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
  darkMode: {
    dark: { ...themes.dark },
    light: { ...themes.normal },
  },
  docs: {
    container: DocsContainer,
  },
};

function ThemeWrapper(props) {
  const mode = useDarkMode() ? "dark" : "light";
  const theme = createTheme(getDesignTokens(mode));
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline /> {props.children}
    </ThemeProvider>
  );
}

export const decorators = [
  (Story) => (
    <ThemeWrapper>
      <Story />
    </ThemeWrapper>
  ),
];

keemor avatar Jun 28 '22 11:06 keemor

Hey!

It's time to smoothly change the name to "Storybook 7 docs theming"

In storybook@7 (alpha now) BaseContainer has prop theme, and passing theme is now much easier.

But storybook-dark-mode does not set the class for the body in the new docs page. You can do it yourself. But I don't know how to get the global parameters of the storybook correctly (the way I found does not converge on types and I use ts-ignore).

The result of my research is this:

diff --git a/.storybook/components/DocContainer.tsx b/.storybook/components/DocContainer.tsx
index 2e0d9fb8b..e1be68bf2 100644
--- a/.storybook/components/DocContainer.tsx
+++ b/.storybook/components/DocContainer.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect } from 'react';
 import { DocsContainer as BaseContainer } from '@storybook/addon-docs';
 import { useDarkMode } from 'storybook-dark-mode';
 
@@ -15,24 +15,16 @@ so a workaround is used.
 export const DocsContainer: typeof BaseContainer = ({ children, context }) => {
   const dark = useDarkMode();
 
+  useEffect(() => {
+    // @ts-ignore
+    const { darkClass, lightClass } = context.store.projectAnnotations.parameters.darkMode;
+    const [addClass, removeClass] = dark ? [darkClass, lightClass] : [lightClass, darkClass]
+    document.body.classList.remove(removeClass);
+    document.body.classList.add(addClass);
+  }, [dark])
+
   return (
-    <BaseContainer
-      context={{
-        ...context,
-        storyById: id => {
-          const storyContext = context.storyById(id);
-          return {
-            ...storyContext,
-            parameters: {
-              ...storyContext?.parameters,
-              docs: {
-                theme: dark ? themes.dark : themes.light,
-              },
-            },
-          };
-        },
-      }}
-    >
+    <BaseContainer context={context} theme={dark ? themes.dark : themes.light}>
       {children}
     </BaseContainer>
   );

Tested on version 7.0.0-alpha.19 (everything may change by release)

dartess avatar Aug 15 '22 09:08 dartess

I'm not really sure what the solution is here but if there are any that could be landed in the package feel free to make a PR!

hipstersmoothie avatar Dec 05 '22 20:12 hipstersmoothie

After upgrading to SB7+Next+Webpack my addons channel is no longer working (so no useDarkMode() possible) and didn't find for now the origin (ref: https://github.com/hipstersmoothie/storybook-dark-mode/issues/205).

So for me, the quick solution is to use as wrapper:

import { DocsContainer as BaseContainer } from '@storybook/addon-docs';
import { themes } from '@storybook/theming';
import React, { useEffect, useState } from 'react';

function isDarkInStorage(): boolean {
  const themeString = localStorage.getItem('sb-addon-themes-3');

  if (themeString) {
    const theme = JSON.parse(themeString);

    return theme['current'] !== 'light';
  }

  return false;
}

export const ThemedDocsContainer = ({ children, context }) => {
  const [isDark, setIsDark] = useState(isDarkInStorage());

  const handler = () => {
    setIsDark(isDarkInStorage());
  };

  useEffect(() => {
    window.addEventListener('storage', handler);

    return function cleanup() {
      window.removeEventListener('storage', handler);
    };
  });

  return (
    <BaseContainer context={context} theme={isDark ? themes.dark : themes.light}>
      {children}
    </BaseContainer>
  );
};

(big sorry for this dirty workaround)

sneko avatar Jan 02 '23 21:01 sneko

I was able to get both the args table to work correctly with subcomponents and the controls section to show up in dark mode with this:

    <BaseContainer
      context={{
        ...context,
        storyById: (id) => {
          const storyContext = context.storyById(id)
          return {
            ...storyContext,
            parameters: {
              ...storyContext?.parameters,
              docs: {
                ...storyContext?.parameters?.docs,
                theme: dark ? darkTheme : themes.light,
              },
            },
          }
        },
      }}
    >

thank you

GaroGabriel avatar Jan 10 '23 09:01 GaroGabriel

  1. Create .storybook/DocsContainer.js
import React from "react";
import { DocsContainer as BaseContainer } from "@storybook/addon-docs/blocks";
import { useDarkMode } from "storybook-dark-mode";
import { themes } from "@storybook/theming";

export const DocsContainer = ({ children, context }) => {
  const dark = useDarkMode();

  return (
    <BaseContainer
      context={{
        ...context,
        storyById: (id) => {
          const storyContext = context.storyById(id);
          return {
            ...storyContext,
            parameters: {
              ...storyContext?.parameters,
              docs: {
                ...storyContext?.parameters?.docs,
                theme: dark ? themes.dark : themes.light,
              },
            },
          };
        },
      }}
    >
      {children}
    </BaseContainer>
  );
};

Thanks @fedek6 - This solution also worked for me, but now my source code block is only showing raw code — equivalent to setting the docs source parameter as type: 'code' . Do you know if it's possible to edit the custom DocsContainer to allow for type: 'dynamic'?

mel-miller avatar Feb 23 '23 20:02 mel-miller

@dartess I did nearly the similar but without having to hack with the document.body object:

diff --git a/.storybook/components/DocContainer.tsx b/.storybook/components/DocContainer.tsx
index 84bf99b..abd2e6d 100644
--- a/.storybook/components/DocContainer.tsx
+++ b/.storybook/components/DocContainer.tsx
@@ -13,27 +13,13 @@ import {
 } from '@storybook/theming';
 
 // @see https://github.com/hipstersmoothie/storybook-dark-mode/issues/127#issuecomment-1070524402
-export const DocsContainer: FC<DocsContainerProps> = ({ children, context }) => {
+export const DocsContainer: FC<DocsContainerProps> = ({ children, ...rest }) => {
   const dark = useDarkMode();
 
   return (
     <BaseContainer
-      context={{
-        ...context,
-        storyById: (id) => {
-          const storyContext = context.storyById(id);
-          return {
-            ...storyContext,
-            parameters: {
-              ...storyContext?.parameters,
-              docs: {
-                ...storyContext?.parameters?.docs,
-                theme: dark ? themes.dark : themes.light,
-              },
-            },
-          };
-        },
-      }}
+      {...rest}
+      theme={dark ? themes.dark : themes.light}
     >
       {children}
     </BaseContainer>

The only thing I have to do is to fill the theme prop with the right one.

Why do you need to play with the DOM body here? :thinking:

soullivaneuh avatar Apr 07 '23 14:04 soullivaneuh

@soullivaneuh it was necessary for storybook-dark-mode@2

Now I have the same version as you.

dartess avatar Apr 07 '23 18:04 dartess

Hey!

It's time to smoothly change the name to "Storybook 7 docs theming"

In storybook@7 (alpha now) BaseContainer has prop theme, and passing theme is now much easier.

But storybook-dark-mode does not set the class for the body in the new docs page. You can do it yourself. But I don't know how to get the global parameters of the storybook correctly (the way I found does not converge on types and I use ts-ignore).

The result of my research is this:

diff --git a/.storybook/components/DocContainer.tsx b/.storybook/components/DocContainer.tsx
index 2e0d9fb8b..e1be68bf2 100644
--- a/.storybook/components/DocContainer.tsx
+++ b/.storybook/components/DocContainer.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect } from 'react';
 import { DocsContainer as BaseContainer } from '@storybook/addon-docs';
 import { useDarkMode } from 'storybook-dark-mode';
 
@@ -15,24 +15,16 @@ so a workaround is used.
 export const DocsContainer: typeof BaseContainer = ({ children, context }) => {
   const dark = useDarkMode();
 
+  useEffect(() => {
+    // @ts-ignore
+    const { darkClass, lightClass } = context.store.projectAnnotations.parameters.darkMode;
+    const [addClass, removeClass] = dark ? [darkClass, lightClass] : [lightClass, darkClass]
+    document.body.classList.remove(removeClass);
+    document.body.classList.add(addClass);
+  }, [dark])
+
   return (
-    <BaseContainer
-      context={{
-        ...context,
-        storyById: id => {
-          const storyContext = context.storyById(id);
-          return {
-            ...storyContext,
-            parameters: {
-              ...storyContext?.parameters,
-              docs: {
-                theme: dark ? themes.dark : themes.light,
-              },
-            },
-          };
-        },
-      }}
-    >
+    <BaseContainer context={context} theme={dark ? themes.dark : themes.light}>
       {children}
     </BaseContainer>
   );

Tested on version 7.0.0-alpha.19 (everything may change by release)

This is useful for me. Thanks

zhimng avatar May 12 '23 03:05 zhimng