Uh oh!
There was an error while loading. Please reload this page.
Upgrade to Storybook 10 + Vite, and Twing 7 - #2405
Conversation
🦋 Changeset detectedLatest commit: 120b284 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for cloudfour-patterns ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Groundwork for Storybook 10, which has no webpack framework for HTML -- the last
release of @storybook/html-webpack5 is 8.6.14. The webpack toolchain is therefore
removed rather than upgraded: webpack, babel-loader, css-loader, style-loader,
postcss-loader, sass-loader, mini-css-extract-plugin, raw-loader, val-loader,
@svgr/webpack and twing-loader all go, along with the Storybook 6 addons that no
longer exist at 10.
The twig pipeline (twing-loader + source-inputs-loader + val-loader + a virtual
environment module) collapses into one Vite plugin. It imports twing/light, because
the full build pulls in locutus, which needs Node's Buffer -- webpack 4 shimmed that
automatically and Vite does not. Every filter our templates use works in light.
Rendering stays synchronous, so story functions still return strings.
Twing 7 also lets the util shim from the Node 24 upgrade go, as that comment
anticipated. Two API changes: the filesystem loader is built from an injected fs, and
path namespaces now include the sigil ("@cloudfour", not "cloudfour").
Sass globs are gone too -- see src/index.scss for why they needed custom JS at all.
Story files are migrated in a following commit.Storybook 8 removed `.stories.mdx`, so all 84 files split into a `.stories.js`
holding the story definitions and a `.mdx` docs page referencing them. 253 stories
and 84 docs pages, matching what was there before.
Storybook's own mdx-to-csf codemod could not be used: it parses with MDX 3, which
rejects the top-level `const` declarations MDX 1 allowed -- which every one of these
files relied on. Storybook 10 has also dropped the codemod. The conversion was done
with a purpose-written transformer plus manual fixes for the files it could not
handle.
Alongside the split:
- `require.context` becomes `import.meta.glob`, and `!!raw-loader!x` becomes `x?raw`.
The Icons and Illustrations pages get SVG components from vite-plugin-svgr's
`?react` query.
- MDX 3 only allows import/export at the top level, so a `for` loop building table
rows became `Array.from`, and HTML comments became `{/* ... */}`.
- Storybook renamed the preview root from `#root` to `#storybook-root`; two
decorators reached into it and were silently failing.
- Twig 3 removed the `spaceless` tag, so footnote-link uses the filter instead.
- Prettier is no longer run over `.mdx`. Prettier 2 predates MDX 2/3 and corrupts
these files -- it rewrites `{/* */}` to `{/_ _/}` and glues JSX onto the following
markdown block. The Prettier 3 upgrade should remove that exclusion.
- `n/file-extension-in-import` is off for stories, docs and .storybook. Its autofix
appends extensions to package export subpaths and to Vite query suffixes, breaking
both.
- Rollup's entry glob now skips stories. They became `.js` in this change, so they
were being pulled into the published bundle.
Every story and docs page was checked in a headless browser: all 337 render, and the
Twig source previews still show `{% include %}` snippets with syntax highlighting.Three defects found reviewing the deploy preview. The Modular Scale table showed steps of "-60", "-61" and sizes of "0em". Token values are strings, so `minimumStep + index` concatenated instead of adding. The `for` loop this replaced got away with it because `i++` coerces to a number after the first pass. Fonts were not loading. `src/base/_fonts.scss` points at `../src/assets/fonts/…`, which is correct for the published package -- `dist/standalone.css` ships alongside `src/assets` -- but does not resolve relative to the stylesheet, so Vite left the URL alone and the browser resolved it against the emitted CSS and 404ed. The path is now a configurable `$dir`, defaulting to the value consumers need, with Storybook's own entry point overriding it. Vite resolves the absolute form and emits the fonts as hashed build assets. Sky Nav rendered permanently open with no toggle button after navigating between stories, for two reasons: - The decorator used `useEffect(fn, [])`, which runs once per preview session. That was harmless in Storybook 6, where these stories each got their own iframe, but Storybook 10 renders every story into one document -- so the DOM was rebuilt while the effect never re-ran. Four other stories had the same pattern. - The template's `no-js` class is cleared by an inline `<script>`, which never runs when markup is injected as a string. `initSkyNav` now clears it, since that function running is itself proof JS is available. All 253 stories and 84 docs pages were re-swept: no empty or failed renders.
The bar cell carried style="width: 90%", which squeezed the Step and Size columns down to a single character and wrapped every value over two or three lines. Removing it lets the cells size to their content: the bar column still takes the remaining space, and each row is one line again. Pre-existing, but only became a legibility problem in the narrower Storybook 10 docs container.
Three of the fixes in this branch touch published files rather than tooling, so they need changelog entries. base/fonts gaining a configurable $dir is a new public Sass API, so it carries the minor bump. The default is the previous hardcoded path, and dist/standalone.css is byte-for-byte identical, so nothing changes for consumers who do not set it. footnote-link.twig and sky-nav.ts are both patches: the first restores rendering under Twig 3, which removed the `spaceless` tag, and the second fixes the menu rendering open with no toggle when markup is injected as a string. src/index.scss is excluded from the published files, so the glob removal there needs no entry. src/index-with-dependencies.scss is published, but it imports from node_modules and documents itself as Storybook-only, so it is not consumable.
78e8857 to
5f09c9cCompareThere was a problem hiding this comment.
@spaceninja Is it expected that spacing is missing above the heading?

There was a problem hiding this comment.
Good catch — it's a real change, though not from the MDX conversion (the markdown around that heading is identical to main).
Storybook's docs CSS zeroes the top margin on the first heading of each level in a page, to tighten up the heading that opens it. But it matches on type rather than position (h2:first-of-type), so a mid-page section heading that happens to be the only h2 loses its spacing too. That rule is the same on production — what changed is the block above it. In Storybook 6.5 IconGallery was a flex layout with margin: 0 10px 30px 0 on each item, so the last row left 30px hanging below it. Storybook 7 rewrote it as a gap-based grid with no item margins, which removed that trailing space and left the heading sitting directly on the icons.
I scripted a check across all 84 docs pages and it affects 15 of them, so thank you for spotting it — my initial read was that Icons was the only one. The rest are mostly the Design → Tokens pages, where an h2 follows a code block, plus a few where the first h2/h3/h4 directly follows the heading above it.
Fixed in 120b284 with a one-rule override in .storybook/preview.scss that restores the margin on headings that aren't genuinely first. Pages where a <Canvas> already supplies a larger bottom margin are unaffected, since adjacent margins collapse. All 84 pages verified after the change.
There was a problem hiding this comment.
@spaceninja Is it expected that the URL no longer updates?

There was a problem hiding this comment.
Sorry, I wasn't able to reproduce this — I tried the deploy preview in Firefox and the URL updated as expected on each sidebar click.
One thing that did change and might be related: Storybook 8 renamed the auto-generated docs entry, so ?path=/story/design-icons--page is now ?path=/docs/design-icons--docs. The param still tracks navigation, but older deep links won't resolve.
If it's still happening for you, let me know your Firefox version and I'll dig further.
There was a problem hiding this comment.
On any text-heavy page (Introduction is fine), confirm the body text is in the Source Sans typeface rather than a system fallback like Times or Arial
I'm seeing the following in dev tools when I inspect a heading or paragraph:
font-family: "Nunito Sans", -apple-system, ".SFNSText-Regular", "San Francisco", BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
There was a problem hiding this comment.
To clarify - the examples rendered with our styles should use our font. The Storybook UI does not.
There was a problem hiding this comment.
Ah, apologies, I just noticed the testing instructions are wrong - they pointed you at a page entirely rendered with Storybook UI. A better example is the Sky Nav
gerardo-rodriguez
left a comment
There was a problem hiding this comment.
Seems good to me overall. But I didn't review every file. 🙃
Storybook's docs styles zero the top margin on the first heading of each level to tighten the heading that opens a page, but the selector matches on type rather than position, so a mid-page section heading that happens to be the only one of its level loses its spacing too. This was invisible under Storybook 6.5 because the blocks preceding those headings supplied their own trailing space -- IconGallery, for instance, was a flex layout with a 30px bottom margin on every item, which left a gap below the last row. Storybook 7 rewrote it as a gap-based grid with no item margins, removing that space and exposing the heading rule. Affects 15 of 84 docs pages: the token pages (heading after a code block), Icons and Illustrations (after a gallery), and a handful where the first h2/h3/h4 directly follows the heading above it.
Uh oh!
There was an error while loading. Please reload this page.
) Every non-inline story on a docs page renders in a 100px iframe, so the examples are clipped and scroll internally instead of sizing to their content. The `.stories.mdx` files set a per-story height on the docs block: <Story name="Basic" height="400px"> The CSF3 migration in #2405 did not carry that across. Thirty stories in thirteen files lost their height and fell back to Storybook's 100px default. One, Illustrations, was translated but to `docs.story.height`, which is not a parameter Storybook recognises, so it had no effect either. Storybook 10 spells this `parameters.docs.story.iframeHeight`. The values here are the ones the `.stories.mdx` files used, recovered from the commit before #2405, so this restores the previous rendering rather than picking new numbers. Two notes: - `inline: false` is left alone. Some of those stories disable inline rendering for a reason that is now obsolete -- the `html-to-react` bug #2405 removed -- but others do it so media queries resolve against the example's own viewport, which is still valid. Sorting out which is which is its own change; this only fixes the height. - The Illustrations parameter is removed rather than corrected. That story has never disabled inline rendering, so it has no iframe to size, and it renders at full height already. A valid-looking parameter that does nothing is worse than none.
* Replace gulp with plain Node build scripts
Removes the last of the gulp toolchain: 280 lines across 5 tasks, and 8
packages (gulp, gulp-postcss, gulp-rename, gulp-run, gulp-sass, gulp-svgmin,
through2, tiny-glob), which takes 199 packages out of the tree.
Each task moves to a script in `scripts/`, matching the `.mjs` convention
`.style-dictionary/build.mjs` already set:
- `buildJS` and `buildTypes` were already plain Rollup API, so they only
needed converting to ESM. They share the virtual root entry via
`rollup-root-entry.mjs`.
- `buildSass` calls sass and postcss directly, matching what gulp-sass and
gulp-postcss were doing: compile, run `postcss.config.js`, write
`standalone.css`, then minify that with cssnano.
- `buildTokens` was a 5-line wrapper shelling out to `preprocess:tokens`, so
it is simply gone; the watcher runs the token build directly.
- `watchPreprocess` uses `node:fs.watch` with `recursive: true` rather than
taking on a watcher dependency. It was the only thing in the build needing
one.
`tiny-glob` is replaced by `node:fs.glob`, which finds the same 63 files. The
results are now sorted explicitly: the entry file is built by joining those
paths, so the published bundle and type declarations were quietly depending
on one glob library's traversal order. Sorting reproduces the previous order
exactly.
All of `dist` is byte-identical, as are the generated token files.
The SVG task is the exception. `gulp-svgmin` pins SVGO 2, so taking svgo as a
direct dependency means SVGO 4 -- and SVGO 4 rejects the `.svgo.yml` plugin
format outright rather than warning, so the config fix and the upgrade cannot
be separated. `.svgo.yml` now uses the current format, which has three
consequences:
- `removeXMLNS` finally applies. It has been silently dropped for years,
because it is not a `preset-default` plugin and the old format could only
express preset overrides. These partials are only `{% include %}`d into
HTML, where the namespace is implied.
- `removeViewBox: false` is no longer needed. SVGO 4 dropped that plugin from
`preset-default`, so viewBox is preserved by default -- which is what this
config already asked for, and what #2422 restored in the minified CSS.
- Coordinates round slightly differently. Rendered and compared pixel for
pixel, 55 of the 72 partials are unchanged, and the largest difference in
the rest is 0.53% of pixels at the edges of curves.
Verified with a headless sweep: all 337 entries (249 stories, 82 docs) render
with content and zero page errors.
* Update the project structure in CONTRIBUTING
Beyond replacing gulpfile.js with scripts/, the tree had drifted: Style
Dictionary's files became .mjs in #2422, .storybook/main became .mjs in
#2405, and stories stopped being .stories.mdx in the same PR.
* Add a changeset for the SVG partial changesThe pre-#2405 MDX set `inline={false}` on this story and the CSF migration did not carry it across -- the only `inline` attribute in the whole of the old MDX, and the one genuine drop the #2428 sweep found. No `iframeHeight` alongside it, matching the original: the component measures ~45px against Storybook's 100px default, so this cannot reproduce the clipping #2420 fixed. Part of #2428
Both files disabled inline rendering for reasons that no longer exist, and both trace to the same removed library. Logo's comment names it: html-to-react, which Storybook 6 used to convert story HTML into React elements, dropped CSS custom properties from inline styles -- and Logo sets --logo-align, --logo-justify and --logo-scale that way. Input's comment describes React syncing properties between identical input elements, which only happens if React is managing those nodes, which required the same conversion. html-to-react is gone from node_modules and the lockfile as of #2405, and the Storybook 10 HTML renderer assigns innerHTML with no HTML-to-React step anywhere in @storybook/html or addon-docs. Neither mechanism can apply. Elastic Textarea's iframeHeight goes too: with the story rendering inline there is no iframe to size, and a valid-looking parameter that does nothing is worse than none. Part of #2428
Overview
Moves the pattern library from Storybook 6.5 + webpack 4 to Storybook 10 + Vite, and Twing 3 to Twing 7.
The reason this is a replacement rather than an upgrade: there is no webpack framework for HTML at Storybook 9 or 10.
@storybook/html-webpack5stops at 8.6.14, with no stable 9 or 10 release — Storybook 10 for HTML is Vite-only. So webpack and its loaders are deleted rather than upgraded, which also settles a chunk of the dependency backlog:css-loader,style-loader,postcss-loader,sass-loader,mini-css-extract-plugin,babel-loader,raw-loader,val-loader,@svgr/webpackandtwing-loaderall leavepackage.json. Vite handles Sass, PostCSS and asset URLs natively, so the ~70-line custom webpack config in.storybook/main.jsis gone too.This also fixes the problem that kept us on 6.5.9 specifically. #2006 was closed because Twig source-preview syntax highlighting broke on a 6.5.16 patch bump, and that was never resolved upstream. Storybook 10 exposes
SyntaxHighlighter.registerLanguageas a supported hook, so registering the Twig grammar is a one-liner rather than a reach intoreact-syntax-highlighter's internals. The previews work, with highlighting.Why the Sass
@uselines needed custom JavaScriptWorth explaining, because the premise looks wrong at first glance —
@useis stock Sass and has never been a problem.Globs in
@useare not a Sass feature. Sass has never supported glob patterns in@use,@forwardor@import, and that is a deliberate design decision, not an oversight. So this, insrc/index.scss:was never Sass doing anything. It only worked because
glob-sass-importer.jsintercepted the pattern and synthesised the file list in JavaScript. Two other details were tells that we were working against the grain: theas componentsaliases existed only because Sass cannot derive a namespace from a glob pattern, and nothing in the codebase ever referenced those namespaces — zero usages across every.scssfile. The importer's entire job was producing 57@forwardlines that are identical on every build.The cost of that jumped sharply here. Sass has two importer APIs, and Vite and gulp-sass 6 both use the modern one. A legacy importer returned
{contents}and Sass resolved nested imports relative to the importing file. A modern importer instead owns a URL namespace: everything reached through your importer comes back to your importer to resolve, and Sass never falls back to its own filesystem loader. Expanding one glob therefore made us responsible for resolving the entire subtree beneath it — I had a working version, and it required reimplementing Sass's own_partialandindexlookup rules by hand.Since all 57 stylesheets are committed source files with nothing generated among them, there was no reason for that list to be computed at build time at all.
src/index.scssnow lists them explicitly.glob-sass-importer.jsis deleted, neither Vite nor gulp-sass needs any Sass configuration, and every@use/@forwardline in the repo is now plain Sass. Adding a stylesheet means adding a line, and load order becomes an intentional choice rather than a side effect of alphabetical globbing.Story files
Storybook 8 removed
.stories.mdx, so all 84 files split into a.stories.js(the story definitions) plus a.mdx(the prose). 253 stories and 84 docs pages, matching what was there before.Storybook's own
mdx-to-csfcodemod could not do this: it parses with MDX 3, which rejects the top-levelconstdeclarations MDX 1 allowed — and every one of these files used them. Storybook 10 has also dropped the codemod from its CLI. The conversion used a purpose-written transformer plus manual fixes for the files it could not handle.Decisions a reviewer might question
@types/nodeoverride, following the rebase onto Upgrade the library build to Rollup 4 and TypeScript 5 #2406. This branch previously pinned@types/nodeto 20.19.9. Webpack 4's dependency tree had been holding it at 12.20.6, and removing webpack let it float to 26.2.0, which TypeScript 4.9 cannot parse — pinning was the cheaper option than dragging the whole library-build upgrade in here. Now that Upgrade the library build to Rollup 4 and TypeScript 5 #2406 has landed TypeScript 5.9 onmain, the pin is unnecessary:@types/nodefloats to 26.2.0 andnpm run typepasses. Rebasing also deduplicatedrollup— this branch previously carried 2.80 as a devDependency alongside the 4.x copy Vite 8 pulls in, and there is now a single 4.62.5..mdx. Prettier 2 predates MDX 2/3 and actively corrupts these files: it rewrites the{/* ... */}comments MDX 3 requires into{/_ ... _/}, reading the asterisks as markdown emphasis, and glues self-closing JSX onto the following markdown block. The Prettier 3 upgrade should remove the exclusion.n/file-extension-in-importis disabled for stories, docs and.storybook. Its autofix appends extensions to things that are not file paths — package export subpaths (storybook/preview-api→.js) and Vite query suffixes (x.twig?raw→?raw.twig) — so the fix introduces the bug. Disabling beat annotating ~120 imports.src/**/*.stories.jsis excluded from type checking. Converting MDX to JS brought 250+ story functions undercheckJsfor the first time, producing 269 errors. Stories are demo code, are not published, and were not type-checked as MDX either, so this keeps the previous coverage rather than widening this PR. Type-checking them is a worthwhile follow-up.storybook-addon-outlineandstorybook-mobileare React 17-only with no Storybook 10 release; outlines are built into Storybook core now, and the other two were already commented out inmain.jsas broken upstream.@whitespace/storybook-addon-htmlhas a v9 that supports Storybook 10 but needs Prettier 3, so it should come back with that upgrade.@etchteam/storybook-addon-statusis upgraded and kept.twing/light, nottwing. The full build pulls inlocutus, which needs Node'sBuffer. Webpack 4 shimmed that automatically; Vite does not, and the bundle dies on load. All 24 filters our templates use behave identically inlight.Two decorators were also reaching for
#root, which Storybook renamed to#storybook-rootin v7, and were silently doing nothing. Fixed.What this changes for consumers
Mostly nothing — the webpack-to-Vite swap is invisible outside the repo. But three fixes landed in files we publish, so there are changesets for them, adding up to a minor bump.
base/fontsgains a$dirvariable (minor). Fonts 404ed in Storybook.src/base/_fonts.scsspointed at../src/assets/fonts/…, which is correct for the published package —dist/standalone.cssships alongside thesrc/assetsdirectory — but does not resolve relative to the stylesheet, so Vite left the URL alone and the browser resolved it against the emitted CSS. Rather than hardcode a second path, the directory is now a configurable$dir, defaulting to the value consumers need, with Storybook's own entry point overriding it. That default meansdist/standalone.cssis byte-for-byte identical tomain; the only difference is that consumers can now override the location.Footnote Link renders under Twig 3 again (patch). It used the
spacelesstag, which Twig 3 removed, so the template could not render at all. It now uses thespacelessfilter, supported since Twig 2.9. Output is unchanged.Sky Nav works when its markup is injected as a string (patch). The template's
no-jsclass is cleared by an inline<script>, which only runs when the browser parses the markup. Anything injecting the template as a string skipped it, leaving the menu permanently open with no toggle button.initSkyNavnow clears the class itself, since that function running is proof JS is available.Two published files changed without needing a changelog entry:
src/index.scssis excluded from the package'sfiles, andsrc/index-with-dependencies.scss— which sets the$diroverride — imports fromnode_modulesand documents itself as Storybook-only, so no consumer can use it.Screenshots
Testing
Every story and docs page was checked in a headless browser — all 337 render, with no console errors. That leaves the things a machine cannot judge: whether pages look right, and whether the interactive bits still work.
Start with the deploy preview:
Then the Twig source previews, which are what broke last time:
{% include '@cloudfour/…' %}— not raw rendered HTMLThen interactivity and layout, since these use decorators that this PR touched:
Fonts are worth a look of their own, since the path handling changed:
Finally, a check on the published package, since the build changed:
npm run build, then confirmdist/standalone.cssis roughly 470KB and contains component styles (grep -c c-badge dist/standalone.cssshould be non-zero)grep -o 'url([^)]*fonts[^)]*)' dist/standalone.css | sort -u | head -1should still show a path beginning../src/assets/fonts/dist/cloudfour-patterns.mjscontains no story code (grep -c stories dist/cloudfour-patterns.mjsshould be 0)