Subissue of #574. Wave 1.
Problem
Emitted stories are titled by emitter — React/Button, WebComponents/Button — so one component appears twice in the navigation and a reader has to know which tree they are in. The system has one Button. Storybook should show one Button, with the frameworks as views of it.
Resolved: the host side works
A spike on specs-testing/workspaces/de-library (Storybook 8.6, branch spike/storybook-framework-tabs, uncommitted) has the whole interaction working. Everything below is verified in a browser, and all of it is host configuration — two files, .storybook/manager.tsx and .storybook/preview.tsx, plus one staticDirs entry in main.ts.
| Capability |
Mechanism |
| Per-framework canvas tabs — React · Web Components · Specs |
addons.add(id, { type: types.TAB, route, match, render }). route/match receive storyId, and useStorybookState() gives it live inside render, so a globally registered tab resolves the selected component |
| Web Components view |
Derives the sibling story id by prefix swap and renders iframe.html?id=<sibling>. Under a docs entry it shows the whole Web Components docs page — its own Controls table, its own stories |
| Specs view |
Fetches the workspace's specs/<dir>/<file>.yaml over a static dir, rendered with SyntaxHighlighter and TabsState from storybook/internal/components, with api / variants / examples as sub-tabs |
| Component is the sidebar link |
docs: { docsMode: true }, with defaultName: 'Overview' naming the parent's own page where a component has subcomponents |
| One Components section |
api.experimental_setFilter hides the Web Components tree from the sidebar while leaving it in the index for the tab |
| Section order |
storySort.order: ['Overview', 'Foundations', 'Components', 'Analysis'] |
| Per-story controls on the docs page |
docs.canvas with withToolbar: false and an additionalActions entry that recovers the story id from the Canvas block's anchor and opens it full screen |
Remaining work
One transform change, in react-from-specs: emit title: 'Components/<Name>' instead of title: 'React/<Name>', subcomponent segments included. Nothing else in either transform changes.
Everything else is host configuration, so it belongs to #607 (document it) and #610 (template it) rather than here. Close this issue when the title change ships and #607 carries the configuration above.
Findings that constrain the design
Four things the spike settled, each of which contradicts a reasonable first guess:
- Identical titles across the two trees do not work. Matching titles produce matching story ids, and Storybook rejects duplicates. The Web Components tree keeps its own titles; the tab maps between the two id spaces by prefix swap. The invariant that actually matters is slug and story-name parity between trees, which already holds —
Default, AlertWithActions and the rest exist in both. That parity is incidental today and needs a test if the tabs ship, or a divergence in one emitter silently drops the reader onto a missing story.
tags: ['!dev'] does not remove stories from the sidebar here. The tag lands in the index and the section renders anyway. experimental_setFilter is what works, which means hiding the Web Components tree costs the emitters nothing.
- The canvas tab label cannot be a story parameter. Set as
parameters.previewTabs, the manager only learns the rename once the preview reports that story's parameters, so the tab reads "Canvas" on every navigation and corrects itself a moment later. addons.setConfig({ previewTabs: … }) in the manager is known before any story loads. Since the sidebar only ever shows the React tree, the label is a scaffold decision keyed on which trees a workspace has — not something an emitter can know.
- A component with subcomponents is a folder, not a link. Storybook has no node that is both leaf and parent, so
DE Alert expands while DE Avatar opens. Accepted as-is: subcomponents stay in the navigation, and defaultName: 'Overview' names the parent's own page.
Compatibility risk
types.TAB is marked @unstable in Storybook 8's own types — "might be removed in the future". The host also imports from storybook/internal/manager-api, storybook/internal/components, and storybook/internal/theming. It is what the built-in Docs tab uses, so none of this is obscure, but a scaffold that writes this code couples customers to internal entrypoints of a tool we do not control. Recorded in #574's ADR A section as a compatibility question rather than left here.
Acceptance criteria
Case data
- Workspace: de-library
- Territory: cli
- Size: m
Subissue of #574. Wave 1.
Problem
Emitted stories are titled by emitter —
React/Button,WebComponents/Button— so one component appears twice in the navigation and a reader has to know which tree they are in. The system has one Button. Storybook should show one Button, with the frameworks as views of it.Resolved: the host side works
A spike on
specs-testing/workspaces/de-library(Storybook 8.6, branchspike/storybook-framework-tabs, uncommitted) has the whole interaction working. Everything below is verified in a browser, and all of it is host configuration — two files,.storybook/manager.tsxand.storybook/preview.tsx, plus onestaticDirsentry inmain.ts.addons.add(id, { type: types.TAB, route, match, render }).route/matchreceivestoryId, anduseStorybookState()gives it live insiderender, so a globally registered tab resolves the selected componentiframe.html?id=<sibling>. Under a docs entry it shows the whole Web Components docs page — its own Controls table, its own storiesspecs/<dir>/<file>.yamlover a static dir, rendered withSyntaxHighlighterandTabsStatefromstorybook/internal/components, withapi/variants/examplesas sub-tabsdocs: { docsMode: true }, withdefaultName: 'Overview'naming the parent's own page where a component has subcomponentsapi.experimental_setFilterhides the Web Components tree from the sidebar while leaving it in the index for the tabstorySort.order: ['Overview', 'Foundations', 'Components', 'Analysis']docs.canvaswithwithToolbar: falseand anadditionalActionsentry that recovers the story id from the Canvas block's anchor and opens it full screenRemaining work
One transform change, in react-from-specs: emit
title: 'Components/<Name>'instead oftitle: 'React/<Name>', subcomponent segments included. Nothing else in either transform changes.Everything else is host configuration, so it belongs to #607 (document it) and #610 (template it) rather than here. Close this issue when the title change ships and #607 carries the configuration above.
Findings that constrain the design
Four things the spike settled, each of which contradicts a reasonable first guess:
Default,AlertWithActionsand the rest exist in both. That parity is incidental today and needs a test if the tabs ship, or a divergence in one emitter silently drops the reader onto a missing story.tags: ['!dev']does not remove stories from the sidebar here. The tag lands in the index and the section renders anyway.experimental_setFilteris what works, which means hiding the Web Components tree costs the emitters nothing.parameters.previewTabs, the manager only learns the rename once the preview reports that story's parameters, so the tab reads "Canvas" on every navigation and corrects itself a moment later.addons.setConfig({ previewTabs: … })in the manager is known before any story loads. Since the sidebar only ever shows the React tree, the label is a scaffold decision keyed on which trees a workspace has — not something an emitter can know.DE Alertexpands whileDE Avataropens. Accepted as-is: subcomponents stay in the navigation, anddefaultName: 'Overview'names the parent's own page.Compatibility risk
types.TABis marked@unstablein Storybook 8's own types — "might be removed in the future". The host also imports fromstorybook/internal/manager-api,storybook/internal/components, andstorybook/internal/theming. It is what the built-in Docs tab uses, so none of this is obscure, but a scaffold that writes this code couples customers to internal entrypoints of a tool we do not control. Recorded in #574's ADR A section as a compatibility question rather than left here.Acceptance criteria
Components/<Name>titles.api,variants, andexamplesformatted, not raw.Case data