Stories

Write stories for your components and pages, wrap them in providers and mocks with a preview file, and know how story ids are made.

A story is one state of a component or page, rendered with mock data. Stories live in *.stories.tsx files that match the stories globs in stageboard.config.ts, and stageboard shows them under Screens and Components.

src/components/button.stories.tsx
import type { Meta, Story } from "stageboard"
import { Button } from "./button"

export default {
  title: "Primitives/Button",
  component: Button,
  args: { children: "Save" },
} satisfies Meta<typeof Button>

export const Primary: Story<typeof Button> = {}
export const Disabled: Story<typeof Button> = { args: { disabled: true } }

This file has two stories, primitives-button--primary and primitives-button--disabled.

The default export: Meta

The default export describes every story in the file. Import the Meta type from stageboard and use satisfies Meta<typeof Component>, so args are checked against the component's props.

FieldTypeWhat it does
titlestringGroup and name in the sidebar, with / for nesting. Also the first half of every story id.
componentcomponentRendered with the merged args when a story has no render.
argsPartial<Props>Props shared by all stories in the file.
parametersobjectDefaults for every story in the file (see below).
decoratorsDecorator[]Wrappers for every story in the file.

parameters takes these fields:

OptionTypeDefaultWhat it does
layout"centered" | "padded" | "fullscreen""fullscreen" with page, otherwise "centered"How the story sits in the frame.
viewport{ width, height? }640 × 360 for componentsThe frame a component story renders in. A missing height is 16:9 to the width. See Component frames.
theme"light" | "dark"noneForces the color scheme (see Screens).
backgroundstringnoneCSS background of the frame's body.
pageobjectnoneMakes the story a screen. See Screens.
variantsRecord<string, string>noneVariant to show per boundary, as { "landing.hero": "hero.calm" }. See Variants.

Component frames

A component story renders in its own frame, parameters.viewport, not at the Desktop, Tablet or Mobile size the device switcher sets for screens. Components shows every frame scaled to fit the same 16:9 tile, so a small button fills its tile as well as a whole panel does. The flow-style viewer and stageboard shot render the story at its frame too. Inside the frame, the story is a page of that size, so its CSS breakpoints apply to the frame's width.

Set the frame on the default export for all of a file's stories, and on a story that needs a different one:

src/components/button.stories.tsx
export default {
  title: "Primitives/Button",
  component: Button,
  parameters: { viewport: { width: 880, height: 120 } },
} satisfies Meta<typeof Button>

export const Variants: Story<typeof Button> = { render: () => <ButtonRow /> }
export const Primary: Story<typeof Button> = { parameters: { viewport: { width: 200, height: 120 } } }

Make a frame just large enough for what the story draws, plus 40px on each side, and leave room for a menu, popover or dialog it opens. npx stageboard frames measures every component story and prints frames that fit; --write sets them in the story files. Write the frame as numbers, so stageboard can read it without loading the file.

Named exports: Story

Every named export that starts with an uppercase letter is a story. Exports that start with a lowercase letter, or with __, are ignored, so helpers and fixtures can live in the same file.

FieldTypeWhat it does
namestringDisplay name. Defaults to the export name split into words (LongTitle becomes "Long Title").
argsPartial<Props>Merged over the file's args.
parametersobjectMerged over the file's parameters, one level deep.
decoratorsDecorator[]Wrappers for this story only.
render(args, context) => ReactNodeRenders the story instead of component. context has id, title, name, args and parameters.

A story can also be a function; it is used as render:

src/pages/pages.stories.tsx
export const Empty = () => <InboxPage tickets={[]} />

The preview file

The preview file wraps every story. Point the preview option at it (npx stageboard init writes ./stageboard.preview.tsx). It imports global CSS and exports decorators, and optionally parameters that act as defaults below the file's and the story's own.

stageboard.preview.tsx
import "./src/styles.css"
import type { Decorator } from "stageboard"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { MockAuthProvider } from "./stageboard/mocks"

const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })

export const decorators: Decorator[] = [
  (Story) => (
    <QueryClientProvider client={queryClient}>
      <MockAuthProvider user="signed-in">
        <Story />
      </MockAuthProvider>
    </QueryClientProvider>
  ),
]

A decorator is (Story, context) => ReactNode. Story decorators wrap the story first, then the file's, then the preview's, so the preview's are outermost.

Mock data

Pages need data without a backend or login. Mock at the edges, not inside components:

  • Providers. Keep pure UI providers (theme, query client, i18n). Replace anything that authenticates, opens sockets or redirects with the same React context and a static value.
  • HTTP. Mock the client once: an axios adapter, a wrapper around window.fetch for your API paths, or handlers keyed by operation name for generated clients. Unknown endpoints should return an empty value of the right shape, so a page never hangs on a loader.
  • Fixtures. Write factories (makeTicket) that fill every required field, and use one consistent fictional world with realistic names and dates in your product's language. Avoid lorem ipsum.

For Next.js apps, nextPreset shims next/image, next/link, next/navigation, next/font and next/dynamic; see Next.js.

Story ids

An id is kebab(title)--kebab(exportName). Marketing/Hero with export const LongTitle becomes marketing-hero--long-title. The name field does not change the id. Run npx stageboard list to see every id.

Without a title, stageboard derives one from the file path: it drops the .stories.tsx extension and the src, components and app folders, collapses a repeated segment, drops a trailing index, and title-cases the rest. src/components/button/button.stories.tsx becomes Button, and src/features/billing-plan/index.stories.tsx becomes Features/Billing Plan.

stageboard reads titles from the source text without running it, so write title as a string literal, either in export default { … } or in a const meta = { … } that the file exports with export default meta.

Storybook compatibility

The format follows Storybook's Component Story Format: a default export with title, component, args, parameters and decorators, and named object stories with args, render and name. Most CSF 3 stories render unchanged once their types come from stageboard.

What stageboard does not do:

  • No play functions, loaders, argTypes, controls, globals or addons. To script what a user does on a screen, use flow actions.
  • tags is accepted but ignored.
  • parameters other than the ones listed above are passed through to context.parameters but have no effect of their own.
  • Titles and ids come from the source text, so computed titles and re-exported stories are not indexed.

On this page