Storybook: how to import styles for individual stories
Asked Answered
H

3

7

I'm using @storybook/vue": "^6.5.10". My components are styled by the <style> block in the bottom of each .vue file. There are also some global CSS (Sass) files that are compiled by the Rails webpacker.

To reproduce the global CSS files in my stories, I wrote decorators (example below) that recreate various CSS contexts. The problem is that the CSS I load in one decorator is applying to my other components/stories and I don't understand why!

My simplified decorator:

// THESE πŸ‘‡ STYLES ARE SHOWING UP IN STORIES THAT DO NOT USE THIS DECORATOR!
import '!style-loader!css-loader!sass-loader!../../app/javascript/styles/site.sass';

const sitePackDecorator = (story) => {     

    // other irrelevant stuff
        
    return {
      components: { story },
      template: '<story />'
    }
  }
;

export {sitePackDecorator}

Then in my story files, I apply it at the component level like this:

import MyComponent from '../app/javascript/src/site/components/assets/MyComponent'
import { sitePackDecorator } from './utilities/sitePackDecorator';

export default {
  title: 'My Component',
  component: MyComponent,
  parameters: {
    layout: 'fullscreen'
  },
  decorators: [sitePackDecorator]
};
const Template = (args, { argTypes }) => ({
    components: { MyComponent },
    props: Object.keys(argTypes),
    template: '<MyComponent v-bind="$props" />',
  })

Here's my Rails configs for webpack / webpacker:

const { environment } = require('@rails/webpacker')
const { VueLoaderPlugin } = require('vue-loader')
const vue = require('./loaders/vue')
const sass = require('./loaders/sass')
const pug = require('./loaders/pug')
const customConfig = require('./alias')
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

environment.plugins.prepend('VueLoaderPlugin', new VueLoaderPlugin())
environment.loaders.prepend('sass', sass)
environment.loaders.prepend('vue', vue)
environment.loaders.prepend('pug', pug)
environment.config.merge(customConfig)
environment.plugins.prepend("CleanWebpackPlugin", new CleanWebpackPlugin());

module.exports = environment

...and main.js references this rails config like this:

const custom = require('../config/webpack/development.js');

module.exports = {
  "stories": [
    "../stories/**/*.stories.mdx",
    "../stories/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions"
  ],
  "framework": "@storybook/vue",
  webpackFinal: async (config) => {
    return { ...config, module: { ...config.module, rules: custom.module.rules } };
  },
}

Note: I ran yarn storybook --debug-webpack and I see: info => Using implicit CSS loaders. I'm not sure what this means, but sounds potentially relevant.

Things I tried that did NOT work

  1. Moving the import !style-loader!... into the story file
  2. Not adding the offending decorator anything. Even when unused, the styles still get loaded!
  3. Moving the import !style-loader!... down into the arrow function of the decorator. This causes an error because import has to be at the top level.
  4. Changing to import '.../site.sass'
  5. Changing to a named import (import styles from '.../site.sass') and then calling use() on the imported object.
Hanhana answered 23/9, 2022 at 16:44 Comment(10)
you generally shouldn't have to do that inline annotation for the loader at import. are you unable to edit the webpack config itself? but reading your question again, it sounds like you're getting global styles, which kind of makes sense, because that's what the style loader does. i think you want inline-styles? – Fratricide
@Fratricide I tested and it also works if I just do import '.../site.sass'. Did that answer your question? – Hanhana
yeah, the loader chain should already be handled in the webapack config. just brushed up on the style-loader doc and it seems like the default behavior will give you inline style, but you might have an alternate webpack configuration? webpack.js.org/loaders/style-loader can you inspect the dom and see where the style tag ended up in ouput? – Fratricide
@Fratricide It is generating a <style> block in the head of the iframe. I think this part is expected because it's what I see in our production environment, too. But why is that <style> appearing for stories that don't use that decorator? Do all imports apply globally? – Hanhana
@Fratricide In case it matters: I actually do need the inline annotations for imports from node_module packages. It works still works if I remove them from imports of my own sass files – Hanhana
ah, interesting. yeah, so i think usually, any import with the sass loader will compile into the same css string and then will output that into the single style tag with the style loader, so basically all your styles end up in the same style block if they are ever included as part of the require / import graph. but there's several configuration options, so you really have to cross reference your config and docs. – Fratricide
@Fratricide All the webpack(er) configs I know of are included above. I don't really have a mandate to reconfigure our production build configs. I really want Storybook to match production (and not visa versa). I thought I would achieve this by configuring Storybook to share the same webpack settings from Rails... but I can't seem to sidestep this style "leakage" issue. Is there literally no documented way to include styles for a particular story? I've seen hundreds of tutorials for setting them globally in preview.js, but zero for a single story/component. This use case doesn't seem exotic... – Hanhana
Let us continue this discussion in chat. – Fratricide
Was there any kind of resolution to this? I'm facing an almost identical issue. – Touchhole
@BennorMcCarthy No authoritative resolution. But I added my hack-y solution as an answer below in case it helps. It's ugly and I still want to solve this in a better way, but it's getting the job done for now. – Hanhana
H
3

This is the hacky "solution". I don't recommend this, but it's the best I've got at the moment:

import sitePackCss from '!!raw-loader!sass-loader!../path/to/styles/site.sass';

const sitePackDecorator = (story) => {
          
    return {
      components: { story },
      template: `
      <div>
        <component is="style" type="text/css">${sitePackCss}</component>
        <story />
      </div>`
    }
  }
;

export {sitePackDecorator}

This will dump the full contents of the compiled CSS into an inline <style> tag on the individual story. It's not pretty, but it does keep those styles from "leaking" into other stories.

Hanhana answered 9/1, 2023 at 19:55 Comment(0)
P
0

I was experiencing a similar issue where the stylesheets I imported on one Story persisted to others. I'm using the MDX file format so I'm not sure exactly how it will look like in your project, but moving the stylesheets from imports at the top of the file like this:

import independent_styles from "../styles/independent.scss"

to inside of a decorator like this:

<Meta
  title="Pages/Example"
  component={ExampleComponent}
  decorators={[
    Story => {
      // include stylesheets here so they do not persist on other Stories
      const csr = require("../styles/independent.scss")
      return <Story />
    },
  ]}
/>

Solved it for me.

Passant answered 16/1, 2023 at 15:28 Comment(2)
What is csr? Arbitrary? – Laurinelaurita
It's just a var name for that particular stylesheet. – Passant
W
0

I'm using Storybook with Vite, here my solution:

import { Meta, StoryFn } from '@storybook/html';

import componentStyle from '@xxxx/index.scss?inline';

import componentHtml from './index.html?raw';

export default {
  title: '{{framework}}/{{component}}',
  decorators: [
    (story) => {
      // Import the style here to not pollute other framework stories
      const styleElement = document.createElement('style');
      styleElement.textContent = componentStyle;

      return `${styleElement.outerHTML}${story()}`;
    },
  ],
  render: ({ label, ...args }) => {
    return componentHtml;
  },
} as Meta<any>;

export const Default = {} as StoryFn<any>;
Wirephoto answered 23/5, 2023 at 22:45 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.