Utilities

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.

RequirementValue
Storybook9 or 10 (^9 || ^10)
React18 or 19 (^18 || ^19): the panel itself is built with React, the stories can use any framework below
Story frameworksReact, Vue, Angular, Web Components, Svelte
Registrydiff-code-connect.<fileKey>.json from the plugin or an exported sxl-codeconnect.json
Current version2.0.2

Installation

BASH
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.

TS
// .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:

TS
// .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.

TS
// .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:

TS
// .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:

TS
// .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-src for https://www.figma.com and https://*.figma.com, so the iframe can load.
  • Composition JSON. If a tokens/tokens folder 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 on storybook build. The compositionFilePath from the registry then resolves without extra Vite configuration.
  • Stable registry name. If preview imports diff-code-connect.SXL-Components.json, that file is missing, but another diff-code-connect.*.json exists 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:

  1. sxl.figmaNodeId: the entry with this node ID.
  2. sxl.component (or its alias sxl.componentName): the entry with this displayName, case-insensitive.
  3. 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:

TS
export const Default = {
  parameters: {
    sxl: { component: "WButton" },
  },
};

Or by the Figma node ID:

TS
export const Default = {
  parameters: {
    sxl: { figmaNodeId: "1:23" },
  },
};

For prototypes you can pass the data directly, without a registry:

TS
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 pluginIn the Storybook panel
Display namePanel header
Date of the last change of the bindingUpdated <date> under the header
Tokens (Ready / Not ready)Tokens badge with the text Have Tokens or No Tokens
StatusBadge Backlog, In Progress, Ready for Dev or Completed
Metadata with DescriptionDescription block
Design EmbedEmbed 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 fileComposition JSON block with a Copy JSON button
Figma component propertiesAPI table: name, kind, default value and options
Code Connect files, import path and snippetCode Connect block: Import, one line per file with its framework, the snippet template
Node IDNode: <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:

  1. sxl.compositionSources: a map from the repo-relative path to the raw JSON text.
  2. sxl.resolveComposition: your own loader function.
  3. sxl.compositionFetchBaseUrl: fetch() from a static folder.
  4. sxl.compositionDevProxyPrefix: fetch() through a same-origin dev proxy.
  5. /sxl-tokens/…: the folder served by the preset.
  6. The raw file from repository.url of 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)

TS
// .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

TS
// .storybook/main.ts
export default {
  staticDirs: [{ from: "../path/to/compositions", to: "/sxl-compositions" }],
};
TS
// .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:

TS
// .storybook/preview.ts, the prefix must match the proxy path below
export default {
  parameters: {
    sxl: {
      compositionDevProxyPrefix: `${import.meta.env.BASE_URL}__sxl_git_raw/`,
    },
  },
};
TS
// .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:

  1. Set parameters.sxl.debugFigmaEmbed: true in preview and look for [SXL Studio addon] Figma embed messages in the browser console: they contain the resolved embed URL and the iframe load and error events.
  2. Make sure the dev server sends a Content-Security-Policy that allows framing Figma. The preset does it automatically; in a custom viteFinal merge the header yourself (see below).
  3. Check that Open embed in new tab in the panel opens the design.
TS
// .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

ParameterTypePurpose
sxl.registrySxlRegistryRegistry object, set once in preview.ts
sxl.componentstringMatch the entry by displayName
sxl.componentNamestringAlias of component
sxl.figmaNodeIdstringMatch the entry by Figma node ID
sxl.figmaUrlstringDirect Figma URL, no registry needed
sxl.descriptionstringOverride 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.designEmbedbooleanOverride the Design Embed flag of the entry
sxl.compositionJsonbooleanOverride the Composition JSON flag of the entry
sxl.metadatabooleanOverride the Metadata flag of the entry
sxl.compositionSourcesRecord<string, string>Map of repo-relative paths to raw JSON text
sxl.compositionFetchBaseUrlstringBase URL for fetching compositions by path
sxl.compositionDevProxyPrefixstringSame-origin prefix for fetching from private Git through a dev proxy
sxl.resolveComposition(path) => Promise<string | undefined>Custom loader of composition JSON
sxl.debugFigmaEmbedbooleanLog embed diagnostics to the console
sxl.embedUrlMode"auto" | "embed-kit-2" | "legacy"Embed URL strategy

If something does not work

What happenedWhat to do
There is no SXL Studio tabThe 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 configuredparameters.sxl.registry is empty or the import path is wrong
Amber dot and No Figma integration for this componentNo 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 blankThe Content-Security-Policy blocks framing. Merge frame-src with mergeSxlFigmaFrameSrcHeader and enable debugFigmaEmbed
Embed is enabled but no Figma URL is availableThe 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 refreshAllow 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 resolvedEnable Composition JSON in the plugin, pick a composition file and push the registry
Composition auto-fetch failedPrivate Git hosts block CORS. Use compositionSources, compositionFetchBaseUrl with staticDirs, resolveComposition() or a dev proxy with compositionDevProxyPrefix
The registry file got a new nameThe name contains the Figma file key. Update the import or rely on the preset alias for diff-code-connect.SXL-Components.json