Storybook Addon
The @sxl-studio/storybook-addon package adds an SXL Studio panel to Storybook with the Figma embed, description, Tokens and Status badges, composition JSON and Code Connect files of the component behind the current story.
@sxl-studio/storybook-addon adds an SXL Studio tab to Storybook. For the component behind the current story the panel shows the Figma embed, the description, the Tokens and Status badges, the component API, the composition JSON and the Code Connect files.
The data comes from the registry file diff-code-connect.<fileKey>.json. The SXL Studio plugin writes it when you connect a component in the Code Connect tab, you commit the file to the repository, and Storybook reads it. The addon only reads: it changes neither the Figma file nor the registry.
| Requirement | Value |
|---|---|
| Storybook | 9 or 10 (^9 || ^10) |
| React | 18 or 19 (^18 || ^19): the panel itself is built with React, the stories can use any framework below |
| Story frameworks | React, Vue, Angular, Web Components, Svelte |
| Registry | diff-code-connect.<fileKey>.json from the plugin or an exported sxl-codeconnect.json |
| Current version | 2.0.2 |
Installation
npm install @sxl-studio/storybook-addon --save-dev
Setup
1. Register the addon in main
Add the package to addons. The preset ships with the package and is applied automatically.
// .storybook/main.ts
export default {
addons: [
"@storybook/addon-docs",
"@sxl-studio/storybook-addon",
],
};
With a shared Storybook config from an internal package, append to the existing list instead of replacing it:
// .storybook/main.ts
import sharedMain from "@your-org/storybook-vue/main";
const config = {
...sharedMain,
addons: [...(sharedMain.addons ?? []), "@sxl-studio/storybook-addon"],
};
export default config;
If the preset is not picked up, list @sxl-studio/storybook-addon/preset in addons explicitly.
2. Load the registry in preview
Import the registry file and put it into parameters.sxl.registry. The import path depends on your repository, for example ../../tokens/tokens/diff-code-connect.<fileKey>.json.
// .storybook/preview.ts
import registry from "../path/to/diff-code-connect.<fileKey>.json";
export default {
parameters: {
sxl: { registry },
},
};
A direct import is enough for current plugin output. fromDiffCodeConnect(raw) normalises the file explicitly; use it when you prefer one code path or mix registry formats:
// .storybook/preview.ts
import raw from "../path/to/diff-code-connect.<fileKey>.json";
import { fromDiffCodeConnect } from "@sxl-studio/storybook-addon";
export default {
parameters: {
sxl: { registry: fromDiffCodeConnect(raw) },
},
};
With a shared preview, merge parameters so the shared decorators and globals stay intact:
// .storybook/preview.ts
import sharedPreview from "@your-org/storybook-vue/preview";
import registry from "../../tokens/tokens/diff-code-connect.<fileKey>.json";
export default {
...sharedPreview,
parameters: {
...sharedPreview.parameters,
sxl: { registry },
},
};
The file name contains the key of the Figma file. When the plugin starts writing a new name, update the import or rely on the preset alias described below.
3. Restart Storybook
Restart the dev server after every change of main or preview. Open a story of a connected component: the SXL Studio tab gets a green dot and shows the panel.
What the preset does
The preset runs as soon as the addon is listed in addons. No particular folder layout is required.
- Figma embed. The Content-Security-Policy of the Vite dev server gets
frame-srcforhttps://www.figma.comandhttps://*.figma.com, so the iframe can load. - Composition JSON. If a
tokens/tokensfolder with the plugin export exists next to the Storybook package or two levels above.storybook, its files are served at/sxl-tokens/…in dev and copied to the static output onstorybook build. ThecompositionFilePathfrom the registry then resolves without extra Vite configuration. - Stable registry name. If
previewimportsdiff-code-connect.SXL-Components.json, that file is missing, but anotherdiff-code-connect.*.jsonexists in the same folder, the preset adds a Vite alias to it (the first name in sorted order when there are several).
Matching stories to components
The addon picks a registry entry for the current story in this order:
sxl.figmaNodeId: the entry with this node ID.sxl.component(or its aliassxl.componentName): the entry with thisdisplayName, case-insensitive.- Story context: the last segment of the story title, the story name, the story ID and the file path are compared with the names, import paths and file names of the entries. A weak or ambiguous match is rejected, and one registry entry is never applied to all stories.
Usually the story files need no changes: keep the display name in the plugin equal to the component name in Storybook. When the names differ, bind the story explicitly:
export const Default = {
parameters: {
sxl: { component: "WButton" },
},
};
Or by the Figma node ID:
export const Default = {
parameters: {
sxl: { figmaNodeId: "1:23" },
},
};
For prototypes you can pass the data directly, without a registry:
export const Default = {
parameters: {
sxl: {
figmaUrl: "https://www.figma.com/design/abc123?node-id=1-23",
description: "Primary action button",
tokensBool: "true",
readiness: "ready-for-dev",
},
},
};
The dot next to the tab title shows the state: green means the story is linked, amber means no entry matched, grey means parameters.sxl.registry is not set. Stories without parameters.sxl at all get no tab: Storybook hides it.
What the panel shows
The data is filled in the plugin: open the component in the Code Connect tab and use the Storybook Integration block. It has its own Connect and Disconnect buttons, works independently of the Code Connect files and does not require a repository URL. The block also shows a ready Story parameters snippet for the story. Details: Code Connect & Storybook.
| In the plugin | In the Storybook panel |
|---|---|
| Display name | Panel header |
| Date of the last change of the binding | Updated <date> under the header |
| Tokens (Ready / Not ready) | Tokens badge with the text Have Tokens or No Tokens |
| Status | Badge Backlog, In Progress, Ready for Dev or Completed |
| Metadata with Description | Description block |
| Design Embed | Embed block with the Figma iframe and the links Open embed in new tab and Open in Figma Dev Mode; with the embed off, an Open in Figma button |
| Composition JSON and the chosen composition file | Composition JSON block with a Copy JSON button |
| Figma component properties | API table: name, kind, default value and options |
| Code Connect files, import path and snippet | Code Connect block: Import, one line per file with its framework, the snippet template |
| Node ID | Node: <id> at the bottom of the panel |
Loading composition JSON
The plugin writes only compositionFilePath into the registry. The composition JSON itself stays in the repository, and Storybook has to load it at runtime. The addon tries the sources in this order:
sxl.compositionSources: a map from the repo-relative path to the raw JSON text.sxl.resolveComposition: your own loader function.sxl.compositionFetchBaseUrl:fetch()from a static folder.sxl.compositionDevProxyPrefix:fetch()through a same-origin dev proxy./sxl-tokens/…: the folder served by the preset.- The raw file from
repository.urlof the registry (GitLab), then the same path relative to the Storybook root.
When nothing works, the panel says what to configure.
Option A: glob and raw import (Vite)
// .storybook/preview.ts
import raw from "../path/to/diff-code-connect.<fileKey>.json";
import { fromDiffCodeConnect } from "@sxl-studio/storybook-addon";
const sources = import.meta.glob("../packages/ds/**/*.json", {
query: "?raw",
import: "default",
eager: true,
}) as Record<string, string>;
function indexByRepoPath(map: Record<string, string>): Record<string, string> {
const out: Record<string, string> = {};
for (const [key, value] of Object.entries(map)) {
// the key must match compositionFilePath from the registry
const rel = key.replace(/^.*?\/packages\//, "packages/");
out[rel] = value;
}
return out;
}
export default {
parameters: {
sxl: {
registry: fromDiffCodeConnect(raw),
compositionSources: indexByRepoPath(sources),
},
},
};
The keys must equal compositionFilePath from the registry: the same repo-relative path as in the monorepo.
Option B: static folder and fetch
// .storybook/main.ts
export default {
staticDirs: [{ from: "../path/to/compositions", to: "/sxl-compositions" }],
};
// .storybook/preview.ts
export default {
parameters: {
sxl: {
compositionFetchBaseUrl: `${import.meta.env.BASE_URL}sxl-compositions/`,
},
},
};
Option C: private GitLab and CORS
The addon can build a GitLab raw URL from repository.url of the registry, but the browser cannot read a cross-origin response without Access-Control-Allow-Origin. Route the requests through a same-origin dev proxy and point the addon at it:
// .storybook/preview.ts, the prefix must match the proxy path below
export default {
parameters: {
sxl: {
compositionDevProxyPrefix: `${import.meta.env.BASE_URL}__sxl_git_raw/`,
},
},
};
// .storybook/main.ts, adjust target and auth to your host
import { mergeSxlFigmaFrameSrcHeader } from "@sxl-studio/storybook-addon/preset";
export default {
async viteFinal(config) {
const base = await mergeSxlFigmaFrameSrcHeader(config);
return {
...base,
server: {
...base.server,
proxy: {
...base.server?.proxy,
"/__sxl_git_raw": {
target: "https://git.example.com",
changeOrigin: true,
secure: true,
rewrite: (path) => path.replace(/^\/__sxl_git_raw/, ""),
},
},
},
};
},
};
Figma embed
If the iframe stays blank:
- Set
parameters.sxl.debugFigmaEmbed: trueinpreviewand look for[SXL Studio addon] Figma embedmessages in the browser console: they contain the resolved embed URL and the iframe load and error events. - Make sure the dev server sends a Content-Security-Policy that allows framing Figma. The preset does it automatically; in a custom
viteFinalmerge the header yourself (see below). - Check that Open embed in new tab in the panel opens the design.
// .storybook/main.ts
import { mergeSxlFigmaFrameSrcHeader } from "@sxl-studio/storybook-addon/preset";
export default {
async viteFinal(config) {
return mergeSxlFigmaFrameSrcHeader(config);
},
};
The URL format is chosen by sxl.embedUrlMode. auto (the default) uses the legacy www.figma.com/embed URL for Safari on localhost and Embed Kit 2.0 (embed.figma.com) everywhere else; embed-kit-2 and legacy force one format. The addon passes the current Storybook host as embed-host, for example localhost:6006.
If Figma asks you to log in after every refresh, allow embedded content and third-party cookies for figma.com in the browser settings. This is a browser privacy policy, not an addon setting.
Parameters reference
| Parameter | Type | Purpose |
|---|---|---|
sxl.registry | SxlRegistry | Registry object, set once in preview.ts |
sxl.component | string | Match the entry by displayName |
sxl.componentName | string | Alias of component |
sxl.figmaNodeId | string | Match the entry by Figma node ID |
sxl.figmaUrl | string | Direct Figma URL, no registry needed |
sxl.description | string | Override the description |
sxl.tokensBool | "true" | "false" | Override the Tokens badge |
sxl.tokenStatus | "assigned" | "partial" | "none" | Deprecated; mapped to tokensBool |
sxl.readiness | "complete" | "ready-for-dev" | "in-progress" | "backlog" | Override the Status badge |
sxl.designEmbed | boolean | Override the Design Embed flag of the entry |
sxl.compositionJson | boolean | Override the Composition JSON flag of the entry |
sxl.metadata | boolean | Override the Metadata flag of the entry |
sxl.compositionSources | Record<string, string> | Map of repo-relative paths to raw JSON text |
sxl.compositionFetchBaseUrl | string | Base URL for fetching compositions by path |
sxl.compositionDevProxyPrefix | string | Same-origin prefix for fetching from private Git through a dev proxy |
sxl.resolveComposition | (path) => Promise<string | undefined> | Custom loader of composition JSON |
sxl.debugFigmaEmbed | boolean | Log embed diagnostics to the console |
sxl.embedUrlMode | "auto" | "embed-kit-2" | "legacy" | Embed URL strategy |
If something does not work
| What happened | What to do |
|---|---|
| There is no SXL Studio tab | The story has no parameters.sxl. Set parameters.sxl.registry in preview.ts and check that the addon is listed in addons |
Grey dot and SXL Studio not configured | parameters.sxl.registry is empty or the import path is wrong |
Amber dot and No Figma integration for this component | No registry entry matched the story. Compare the display name in the plugin with the story title, or set sxl.component or sxl.figmaNodeId. Make sure the component is connected in the Storybook Integration block and the registry file is up to date |
| The Figma iframe is blank | The Content-Security-Policy blocks framing. Merge frame-src with mergeSxlFigmaFrameSrcHeader and enable debugFigmaEmbed |
Embed is enabled but no Figma URL is available | The registry has no Figma file key. Set the Figma file URL in Repository connection of the plugin and export the registry again |
| Figma asks to log in on every refresh | Allow embedded content and third-party cookies for figma.com in the browser. On Safari with localhost the addon already uses the legacy embed |
No composition data resolved | Enable Composition JSON in the plugin, pick a composition file and push the registry |
| Composition auto-fetch failed | Private Git hosts block CORS. Use compositionSources, compositionFetchBaseUrl with staticDirs, resolveComposition() or a dev proxy with compositionDevProxyPrefix |
| The registry file got a new name | The name contains the Figma file key. Update the import or rely on the preset alias for diff-code-connect.SXL-Components.json |