Styling, CSS, and Static Assets

Purpose and Scope

Storybook renders your components inside a preview iframe, so styling configuration is about making that iframe behave like your application rather than styling the Storybook manager shell. The styling guide frames this as a deliberate mirroring exercise: because applications include CSS in many different ways, the best Storybook setup usually follows the same pipeline, imports, preprocessors, and global styles your app already uses. This page explains the supported patterns for global CSS, bundled CSS, CSS modules, PostCSS, preprocessors, static assets, and theme switching so teams can keep component examples visually faithful to production.

Sources: docs/configure/styling-and-css.mdx

The practical decision is where a style or asset should enter the system. Styles imported from story, component, or preview files flow through the builder and are injected into the preview iframe; they support the development server workflow and hot module replacement when the builder supports it. Styles included through .storybook/preview-head.html are plain document head additions, which is useful for simple global links but does not participate in the same HMR loop. Assets can either be imported as modules so the bundler resolves them, or referenced by public URL when they live in a served static directory.

Sources: docs/configure/styling-and-css.mdx, docs/_snippets/component-story-static-asset-with-import.md, docs/_snippets/component-story-static-asset-without-import.md

Theming is a related but distinct concern. CSS setup gets the right styles into the preview; theme setup lets readers switch between named visual modes while staying inside the same Storybook. @storybook/addon-themes is the repository-supported addon for this workflow, and its README describes it as a way to switch between multiple themes for components inside the preview. The API reference exposes decorator helpers for common theme mechanisms: JSX providers, class names, and data attributes. Those helpers connect Storybook globals and toolbar selection to the actual theme implementation used by the app.

Sources: code/addons/themes/README.md, code/addons/themes/docs/api.md

Relevant Source Files

  • docs/configure/styling-and-css.mdx - Primary documentation for importing global CSS, preview iframe behavior, CSS modules, PostCSS, Sass/Less/Stylus, and Vite versus Webpack configuration expectations.
  • docs/_snippets/component-story-static-asset-with-import.md - Cross-renderer story examples that import an image file from a local static path and pass the resolved URL into a rendered story.
  • docs/_snippets/component-story-static-asset-without-import.md - Cross-renderer story examples that reference an image by public URL, using /image.png when the file is assumed to live in the public directory.
  • code/addons/themes/README.md - Package-level usage documentation for @storybook/addon-themes, including installation, registration in .storybook/main.js, tool-specific recipes, and per-meta or per-story globals.theme overrides.
  • code/addons/themes/docs/api.md - API reference for theme decorators such as withThemeFromJSXProvider, withThemeByClassName, and withThemeByDataAttribute, including option names and behavior.

CSS Entry Points

The recommended global CSS entry point is .storybook/preview.ts or .storybook/preview.tsx. The styling guide says all Storybooks are pre-configured to recognize CSS imports, so a global stylesheet can be imported from preview configuration and applied to all stories. That is the best default because the file is handled as bundled CSS and participates in hot module replacement, letting developers tune styles without restarting the Storybook server. The same principle applies when component files import their own CSS; Storybook’s builder sees those imports while rendering the component states.

Sources: docs/configure/styling-and-css.mdx

A preview import is best when the stylesheet is part of the application bundle, when it depends on a build step, or when it should be updated during active development. By contrast, .storybook/preview-head.html is the escape hatch for static head markup. The guide recommends it for a global CSS file that should be included in every story by adding a tag to the preview iframe head. The tradeoff is explicit: those files are not subject to HMR, so a running Storybook server must be restarted before changes are visible.

Sources: docs/configure/styling-and-css.mdx

// .storybook/preview.ts
import '../src/styles/global.css';
<!-- .storybook/preview-head.html -->
<link rel='stylesheet' href='/global.css' />

CSS modules and processor support depends on the builder. For Vite projects, the guide states that CSS modules, PostCSS, and CSS preprocessors such as Sass, Less, and Stylus are supported out of the box, and existing vite.config.js customizations for CSS modules or PostCSS are automatically applied to Storybook. For Webpack projects, CSS modules, PostCSS, Sass, and Less can require extra configuration. The recommended helper is @storybook/addon-styling-webpack, although projects can also customize Storybook’s Webpack configuration directly when they need full control.

Sources: docs/configure/styling-and-css.mdx

Static Asset Patterns

Story-level static assets have two supported shapes in the supplied snippets. The first imports an asset file directly from the story’s module graph. In the React TypeScript example, imageFile is imported from ./static/image.png, assigned to an object with src and alt, and used from the story render function. This pattern is appropriate when the asset belongs to the component example, should be transformed or fingerprinted by the builder, or should move with the story file during refactors.

Sources: docs/_snippets/component-story-static-asset-with-import.md

import imageFile from './static/image.png';
 
const image = {
  src: imageFile,
  alt: 'my image',
};
 
export const WithAnImage = {
  render: () => <img src={image.src} alt={image.alt} />,
};

The second shape references the asset by URL without importing it. The snippets repeatedly annotate this with the assumption that image.png is located in the public directory, and the story renders /image.png. This pattern is clearer for assets that are already served statically by the application or are shared across many examples. The cost is that the path is no longer checked by the module system, so teams should keep public asset conventions documented and stable across local development, static builds, and published Storybooks.

Sources: docs/_snippets/component-story-static-asset-without-import.md

export const WithAnImage = {
  render: () => <img src='/image.png' alt='my image' />,
};

The snippets show the same conceptual choice across Angular, React, Solid, and Svelte story formats: either import the file and pass the resolved value into props or markup, or rely on a public URL. That matters because Storybook’s story abstraction is renderer-specific but the asset decision is not. When documenting a component, choose the asset pattern that matches the application’s production behavior first, then encode it in the story format for that renderer. This keeps generated docs, interaction tests, and visual review aligned with the same image, font, or media behavior users see in the app.

Sources: docs/_snippets/component-story-static-asset-with-import.md, docs/_snippets/component-story-static-asset-without-import.md

Theme Switching in the Preview

@storybook/addon-themes adds a higher-level theming layer on top of CSS availability. Its README shows the package installation command, registration through the addons array in .storybook/main.js, and recipe links for tools such as Emotion, Material UI, Bootstrap, PostCSS, styled-components, Tailwind, and Vuetify-style custom decorators. This placement is important: the addon is not a CSS processor. It is the Storybook integration point that lets stories render under named themes and lets users switch those themes from the preview experience.

Sources: code/addons/themes/README.md

npm i -D @storybook/addon-themes
export default {
  addons: ['@storybook/addon-themes'],
};

The addon’s README also documents a Storybook-native override mechanism: globals.theme. A component meta export can set globals: { theme: 'dark' } so every story for that component starts in a specific theme, and an individual story can set the same global to override just that case. Use this when a component state only makes sense in a particular color mode, when a regression story must lock a theme, or when a design review needs explicit light and dark variants next to each other rather than relying only on toolbar interaction.

Sources: code/addons/themes/README.md

Theme Decorator API Reference

Theme decorators translate a selected Storybook global into the concrete mechanism your app uses. withThemeFromJSXProvider accepts theme objects and wraps stories with a JSX provider component. Its options include themes, a record from theme name to theme object; defaultTheme, the name to use initially; Provider, the JSX component that provides theme context; and GlobalStyles, a JSX component containing global CSS styles. This is the right match for libraries that expose a React-style provider, such as Emotion-based systems, Material UI, or other context-driven theme systems.

Sources: code/addons/themes/docs/api.md

import { withThemeFromJSXProvider } from '@storybook/addon-themes';
 
export const decorators = [
  withThemeFromJSXProvider({
    themes: { light: lightTheme, dark: darkTheme },
    defaultTheme: 'light',
    Provider: ThemeProvider,
    GlobalStyles: CssBaseline,
  }),
];

withThemeByClassName is for CSS systems where changing a class on a parent element activates the theme. Its required options are themes, a record from theme name to class name, and defaultTheme. It also accepts parentSelector, which defaults to html, so projects can choose the element that receives the theme class. withThemeByDataAttribute follows the same idea for data-attribute-driven systems. It takes named theme values plus an attributeName, such as the Bootstrap-oriented data-bs-theme, and applies that attribute to the selected parent.

Sources: code/addons/themes/docs/api.md

APIUse whenKey options
withThemeFromJSXProviderThemes are provided through a JSX context providerthemes, defaultTheme, Provider, GlobalStyles
withThemeByClassNameTheme selection is controlled by CSS classesthemes, defaultTheme, parentSelector
withThemeByDataAttributeTheme selection is controlled by a data attributethemes, defaultTheme, attributeName, parentSelector

Implementation Guidance and Next Steps

Start by identifying how the application styles components in production. If the app imports global CSS from an entry file, mirror that import in .storybook/preview.ts. If it relies on a static stylesheet tag, add it to .storybook/preview-head.html and remember the restart requirement. If the app uses Vite, expect CSS modules, PostCSS, and preprocessors to follow the Vite configuration. If it uses Webpack, budget time for @storybook/addon-styling-webpack or a custom Webpack rule. This keeps the styling layer boring, predictable, and close to the real application pipeline.

Sources: docs/configure/styling-and-css.mdx

Next, decide how static assets should be addressed in stories. Use imported assets for examples that should be bundled with the story module and public URLs for assets that intentionally live in the served public directory. Finally, add @storybook/addon-themes when teams need interactive theme switching or repeatable theme-specific stories. Choose the decorator that matches the app’s theme mechanism rather than changing the app to fit Storybook. Related reading: configure overview for the broader .storybook file model, story rendering and layout for preview iframe customization, and the Vite or Webpack builder pages for deeper build pipeline changes.

Sources: docs/_snippets/component-story-static-asset-with-import.md, docs/_snippets/component-story-static-asset-without-import.md, code/addons/themes/README.md, code/addons/themes/docs/api.md