Skip to content

feat(storybook): modularize CSS module support as pluggable preset config#36088

Merged
Hotell merged 26 commits intomicrosoft:masterfrom
Hotell:headless/css-modules-sb
May 5, 2026
Merged

feat(storybook): modularize CSS module support as pluggable preset config#36088
Hotell merged 26 commits intomicrosoft:masterfrom
Hotell:headless/css-modules-sb

Conversation

@Hotell
Copy link
Copy Markdown
Contributor

@Hotell Hotell commented May 4, 2026

NOTE: all changes are consolidated in c56b5d1

Summary

Makes the CSS module "Show code" support from #36073 pluggable and zero-boilerplate — story authors just import a CSS module and everything works.

Before this change, every story file in #36073 required manual ?raw imports and a withCssModuleSource() call to register its CSS into the docs panel. Now the babel plugin has opt-in configuration to enable auto-detection of *.module.css imports at build time, and all configuration lives in a single PresetConfig object passed to the sandbox addon.

Architecture

flowchart TB
  subgraph "Consumer — stories/.storybook/main.js"
    MC["PresetConfig\n{ importMappings, cssModules: { tokensFilePath } }"]
  end

  subgraph "Addon — react-storybook-addon-export-to-sandbox"
    WP["webpack.ts\ncreates babel-loader rule"]
  end

  subgraph "Babel Plugin — babel-preset-storybook-full-source"
    FP["fullsource.ts\nAST walk → detects *.module.css imports"]
    MI["modifyImports.ts\nrewrites paths to ./styles/<basename>"]
  end

  subgraph "Output — Story Parameters (injected per-story)"
    P1["parameters.fullSource"]
    P2["parameters.cssModuleSources\n{ cssModules: [{name, source}], tokensSource }"]
  end

  subgraph "Runtime — HeadlessSourcePanel"
    HP["Reads parameters → renders tabbed code view"]
  end

  MC -->|"options"| WP
  WP -->|"[plugin, { importMappings, cssModules }]"| FP
  FP --> MI
  FP -->|"reads .module.css from disk"| P2
  FP -->|"injects cleaned source"| P1
  P1 --> HP
  P2 --> HP
Loading
flowchart LR
  subgraph "DRY Config (re-export pattern)"
    SP["stories/.storybook/main.js\n(source of truth)"]
    DP["apps/docsite/.storybook/main.js\n(extends stories config)"]
    SPV["stories/.storybook/preview.js\n(tokens, decorators, docs page)"]
    DPV["apps/docsite/.storybook/preview.js\n(extends stories preview)"]
  end

  DP --> SP
  DPV --> SPV
Loading

What changed (vs #36073)

Pluggable preset config

PresetConfig now accepts a cssModules option that flows through the addon's webpack preset into the babel plugin:

// stories/.storybook/main.js
loadWorkspaceAddon('@fluentui/react-storybook-addon-export-to-sandbox', {
  options: {
    importMappings: getImportMappingsForExportToSandboxAddon(),
    cssModules: { tokensFilePath: path.resolve(__dirname, 'tokens.css') },
  },
});

Auto-detection replaces manual wiring (opt in behind config flag)

The babel plugin walks each *.stories.tsx AST, finds *.module.css imports, reads them from disk, and injects parameters.cssModuleSources per-story.

✅ Result: 30 story files lost their boilerplate.

Before (each story file):

import { withCssModuleSource } from '../_helpers/withCssModuleSource';
import rawCss from './accordion.module.css?raw';

export default {
  ...withCssModuleSource({ rawCss, name: 'accordion.module.css' }),
};

After:

import classes from './accordion.module.css';

export default {
  title: 'Headless Components/Accordion',
  component: Accordion,
};

DRY storybook configs

The docsite app (apps/public-docsite-v9-headless) no longer duplicates webpack/preview setup — it re-exports from the stories package config and only adds deployment-specific overrides (stories paths, staticDirs, build.previewUrl, storySort).

Co-located storybook infrastructure

Moved into stories/.storybook/ (source of truth):

  • tokens.css — design tokens (was theme/tokens.css)
  • HeadlessDocsPage.tsx — custom docs page (was in app)
  • HeadlessSourcePanel.tsx — tabbed code panel (was in app)
  • headless-docs-page.css — docs page chrome (was in app)
  • theme.js — Storybook theme (was in app)

The preview-stories package re-exports these for app consumption.

Removed dead code

Deleted Reason
withCssModuleSource.ts + raw.d.ts Replaced by auto-detection
cleanSource.ts Inlined into fullsource plugin
rawQueryPlugin (esbuild) No more ?raw imports in stories
?raw resourceQuery webpack guards Unnecessary after Storybook 8 asset handling

Renamed parameter namespace

parameters.themeparameters.cssModuleSources

  • avoids collision with VR testing's parameters.theme (which carries Fluent theme objects like webDarkTheme)
  • provides domain boundary related to css modules enablement

Test coverage

  • 17 babel plugin fixture tests (new: css-module-auto-detect, css-module-with-tokens)
  • 33 sandbox addon tests (updated for new cssModuleSources shape)
  • SSR esbuild plugin unit tests (simplified after rawQueryPlugin removal)

tudorpopams and others added 25 commits May 4, 2026 10:32
…me, refine sandbox & tabs

Bebop docsite (apps/public-docsite-v9-headless):
- Custom BebopDocsPage replaces autodocs: title/description/primary canvas/
  ArgTypes/remaining stories layout matching deployed Fluent docs.
- BebopSource portals a tabbed Story.tsx + .module.css source panel into the
  same .sbdocs-preview canvas card the user already sees, listening to
  Storybook's native 'Show code' toggle so it sits next to 'Open in Stackblitz'.
- Per-story tab strip is filtered to CSS modules actually referenced in the
  displayed TSX (the meta still lists the full set so the sandbox can bundle
  what's needed).
- Sidebar/toolbar accent + tab indicator + selected nav now use Bebop
  magenta #9b1f5a (Figma --prmt-color-red ramp); replaces the prior near-black
  #4a0a2c. Sidebar hover bg switched to neutral grey.
- Drop the rsms.me Inter import; everything inherits Segoe UI.

Stories (react-headless-components-preview/stories):
- New README.md replaces CLAUDE.md, merging the package metadata header with
  the full authoring guide (pattern, boilerplate, token tiers, gotchas,
  PR checklist, file map).
- withStorySource pins each story's own raw source as
  parameters.docs.source.code+originalSource and overrides fullSource via a
  non-writable getter so the babel preset's empty post-strip overwrite is
  swallowed; rewrites long ../../bebop/components/*.module.css paths to a
  colocated ./styles/*.module.css for paste-ready snippets.
- withCssModuleSource bundles tokens.css + only the CSS modules referenced
  from src/example.tsx into the Stackblitz sandbox under src/styles/, and
  prepends an import of tokens.css to App.tsx.
- Remove the broken Copy Page button.

Babel preset (babel-preset-storybook-full-source):
- modifyImportsPlugin no longer warns for known-safe relative patterns
  (*.module.css, ?raw queries, with{Story,CssModule}Source helpers,
  *.stories?raw). Strip behaviour and tests are unchanged.

Recovered bebop/ design system source (tokens.css + 27 component CSS modules)
that drives the docsite preview and Stackblitz sandboxes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…imer

- Rename bebop/ folder to theme/ (tokens.css + 27 component CSS modules)
- Rename BebopDocsPage -> HeadlessDocsPage and BebopSource -> HeadlessSourcePanel
- Scrub all bebop identifiers, paths, classes, and comments across stories,
  helpers, .storybook config, and project.json files
- Add a disclaimer banner on the docs page explaining that headless components
  ship without default styles and that the demo CSS is illustrative only

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move every `theme/components/*.module.css` into the corresponding story
folder under `stories/src/<Component>/`. Stories that compose multiple
components (Field, Dialog, Select, MessageBar, SpinButton) reference the
adjacent folder's module via a sibling import.

- Update `withStorySource` and `withCssModuleSource` regexes to match any
  relative `*.module.css` import (not just the old `theme/components/`
  path) and rewrite to `./styles/` for paste-ready snippets and Stackblitz
  sandbox layout.
- `theme/` now only contains `tokens.css` (still global).
- Update the stories README to document the colocated layout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The global tokens.css now lives under apps/public-docsite-v9-headless/theme/
together with the docsite that consumes it. Update import paths in:
  - apps/public-docsite-v9-headless/.storybook/preview.js
  - stories/.storybook/preview.js
  - stories/src/_helpers/withCssModuleSource.ts (?raw inline)
  - project.json build inputs (workspaceRoot -> projectRoot for the docsite)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Owning tokens.css inside the stories package (where the headless story
authors edit theme variables) is cleaner than reaching from a sibling
package into the docsite app. The docsite now imports it from there.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the design-system swap suggestion; just clarify that the CSS in the
stories is a demonstration of one possible look.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…review

The standalone stories storybook lives at stories/.storybook/preview.js,
so tokens.css (now at stories/theme/tokens.css) is one level up via
../theme/tokens.css — not ../../theme. The docsite build still passes
because it imports via its own preview.js which uses a different relative
path.

Also add a beachball change file for the babel-preset-storybook-full-source
warning skip (committed earlier).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-check errors

- Exclude react-headless-components-preview-stories from auto-inferred test-ssr
  target via nx.json workspace plugin config (esbuild can't resolve ?raw query
  imports used by withStorySource).
- Narrow language type in HeadlessSourcePanel to satisfy SyntaxHighlighter's
  SupportedLanguage union (was string).
- Type RatingIcon helpers as React.FC instead of () => React.ReactNode so they
  match RatingDisplay's icon prop under React 17/18 stricter typing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ton underline

- Bump heads-up disclaimer font to 16px and beef up the styling so it stands
  out on every story page.
- Add a secondary preview note clarifying the controls are in preview and
  their APIs are subject to change.
- Force the 'Show code' and 'Open in Stackblitz' buttons' hover/focus
  underline to the magenta accent (Storybook's default and the sandbox
  addon's hard-coded blue both override here).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The .storybook/tsconfig.json runs with checkJs:true so the webpack config
in main.js needed JSDoc annotations: `patchRules` parameter is annotated
as `any[]` and the `localConfig` cast as `any` to access `module.rules`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Many *.module.css files in the headless stories had selectors written as
their bundler-generated names (e.g. .dialog-module__row--0+lMS) or with
trailing dashes/escaped pluses (.bar-, .card\+). These selectors never
matched at runtime, leaving Dialog, Divider, MessageBar, Rating,
SearchBox, Skeleton, Slider and others without their demo styles.

- Strip the '<file>-module__' prefix and '--<hash>' suffix
- Strip stray trailing '-' / escaped '+' from selectors
- Combine .surface with .alertSurface in the alert / non-modal Dialog
  stories so the surface is positioned and styled correctly

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Skeleton demo had an extra 140px 'thumb' bar and a shimmer animation
that didn't match the deployed reference. Rewrite the CSS module to match
the deployed look: a clean card with avatar + two short lines, then three
horizontal bars with a softer pulse animation. Drop the unused .thumb.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…le.css

Removes the `react-headless-components-preview-stories` SSR-test exclusion
added when these tests started failing. test-ssr's esbuild pipeline now
bundles `?raw` queries (raw text loader with extension resolution) and
shims `*.module.css` imports as a Proxy that echoes the property name —
sufficient for SSR snapshots without the actual CSS-Modules transform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ambient `declare module '*?raw'` lived in the global
`typings/static-assets/` allowlist, which is too broad — it pulled the
shape into every consumer's compile. Move it to a colocated `raw.d.ts`
inside the stories package so the typing is opt-in rather than
workspace-wide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the special-case for `*.module.css`, `?raw` queries, and
withStorySource/withCssModuleSource imports that suppressed the
modifyImports warning. The warning is informational and the helpers do
their own thing — silencing them was tightly-coupled wallpaper. Strip
behaviour and the existing 15 tests are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Source of truth for the CSS-Modules + ?raw webpack wiring lives at
`stories/.storybook/css-modules-webpack.js`. Both the per-package
storybook and the docsite app `require()` it — eliminates the duplicated
~80 LoC of `cssModuleRule` + `patchRules` between the two configs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e app

HeadlessDocsPage / HeadlessSourcePanel are docsite-app concerns, not
stories-package concerns — the per-package storybook should not need a
custom docs page. Move both into `apps/public-docsite-v9-headless/.storybook/`
and wire the docsite's preview.js to consume them locally.

Externalize the previously-inlined `<style>` block into a static
`headless-docs-page.css` loaded once from preview.js.

Switch HeadlessSourcePanel from inline CSSProperties objects to
emotion-via-`storybook/theming` `styled` components so the panel inherits
the active SB theme tokens (matches the rest of the docs chrome).

Surface the `CssModule` / `HeadlessSourceParameters` types from
`withCssModuleSource` (which produces them) instead of from the panel
that consumes them — cleaner one-way dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without an explicit webfont, the previous font stack fell through to
BlinkMacSystemFont/Roboto on machines without Segoe installed (i.e.
anyone not on Windows). Pull the four Segoe UI weights (light/normal/
semibold/bold) from Microsoft's static font CDN in both the manager
chrome and the story canvas iframe so the docsite renders Segoe on
every OS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends fullSourcePlugin to also write Story.parameters.docs.source.code
and originalSource on every story export, using the cleaned raw file
contents (with `*.module.css` paths rewritten to the Stackblitz
`./styles/<basename>` layout). Removes the need for per-story
`withStorySource(...)` calls and `?raw` story-source imports.

Sweeps all 42 headless story files to drop the boilerplate, deletes the
`withStorySource` helper, and updates the four fixture outputs to match
the new injection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Card was the only headless component story using inline Tailwind class
strings, which the docsite doesn't load — so /docs/headless-components-card
rendered unstyled. Add a `card.module.css` driven by the same token
ramp (--bg, --border, --space-*, --radius-*, --shadow-*, --accent…) the
rest of the components use, and switch CardDefault / CardSelectable /
CardDisabled to consume it. Wire `withCssModuleSource` in the meta so
the Stackblitz sandbox bundles the new stylesheet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Stories README: §3 now describes the auto-injected docs source flow and
  the corrected `...withCssModuleSource(...)` spread (the previous example
  used a `transform:` shape the helper never matched). §8 paths point at
  the moved `?raw` typing, the shared webpack module, and the docs page
  components in the docsite app.
- Babel preset README: documents the new `parameters.docs.source.code` /
  `originalSource` injection alongside the existing `fullSource` feature.
- test-ssr README: documents the two custom esbuild plugins
  (`?raw` query loader, `*.module.css` Proxy shim) so future contributors
  know they exist before reaching for a testSSR exclusion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Storybook ships a global \`.docs-story + div > div:last-child { background:
rgb(0,0,0) }\` rule meant for the legacy Source block. Our portaled
HeadlessSourcePanel renders into the matched DOM position; the previous
inline \`style=\` on its container beat the rule on specificity, but the
move to \`styled\` from \`storybook/theming\` (commit 7c949d1) emits class
names that don't.

Reset the inherited paint (background / box-shadow / border-radius /
right) on \`.headless-source-portal > div\` so the panel's own light
surface and theme-driven tokens show through. Background uses the
\`--bg-elev\` token from \`theme/tokens.css\`, already loaded in preview.js.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The .preview's negative margins push it to the card's border, but its
own square top corners were poking past the card's rounded boundary —
clearly visible on Selectable/Disabled where the magenta border made
the mismatch obvious. Added overflow: hidden to .card so children
clip to the rounded shape; the border and box-shadow render outside
the overflow box and stay intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both packages were merged with Tailwind class strings inline, which the
docsite doesn't load — same problem we hit with Card. Following the
existing pattern:

- New drawer.module.css covers the OverlayDrawer (fixed-position dialog
  pinned to the right edge) and InlineDrawer (in-flow expanding panel)
  variants, plus shared DrawerHeader / Body / Footer / nav / button
  classes. Native `<dialog>` user-agent styles needed `left: auto` to
  stop centering and let `right: 0` win.
- New popover.module.css consolidates the trigger / surface / heading /
  body / actionButton / menuItem / arrow rules used across all 11
  popover stories. Multi-color trigger variants (root/nested/deep) are
  kept as triggerSecondary + triggerSmall modifiers driven by the
  monochrome accent ramp instead of arbitrary blue/indigo/purple.
- Both meta files now spread `withCssModuleSource(...)` so the docsite's
  "Show code" tab strip and the Stackblitz sandbox bundle the right
  styles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

📊 Bundle size report

✅ No changes found

@Hotell Hotell force-pushed the headless/css-modules-sb branch from 9501585 to 684ac28 Compare May 4, 2026 12:38
@Hotell Hotell changed the title refactor(storybook): modularize CSS module support as pluggable preset config feat(storybook): modularize CSS module support as pluggable preset config May 4, 2026
@Hotell Hotell closed this May 4, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

Pull request demo site: URL

@Hotell Hotell force-pushed the headless/css-modules-sb branch 2 times, most recently from 940253b to 4e8e235 Compare May 4, 2026 17:57
@Hotell Hotell force-pushed the headless/css-modules-sb branch from 4e8e235 to c56b5d1 Compare May 5, 2026 08:49
@Hotell Hotell marked this pull request as ready for review May 5, 2026 08:57
@Hotell Hotell requested review from a team as code owners May 5, 2026 08:57
@Hotell Hotell merged commit 70a3e37 into microsoft:master May 5, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants