Variants

Make a component swappable, add alternatives as *.variant.tsx files, switch between them on the live screen and screenshot each one.

A variant is an alternative implementation of one part of your UI, kept in its own *.variant.tsx file. You mark a component as swappable once; then you or an agent add variants, and you switch between them on the live screen to compare. Production always renders the original.

Make a component swappable

Wrap the implementation with variant(id, Component) from stageboard/react and export the result under the original name:

src/components/hero.tsx
import { variant } from "stageboard/react"

export type HeroProps = { title: string; subtitle: string; cta: string }

function HeroImpl({ title, subtitle, cta }: HeroProps) {
  return (
    <section className="mx-auto max-w-3xl px-6 py-24 text-center">
      <h1 className="text-5xl font-semibold">{title}</h1>
      <p className="mt-4 text-lg text-zinc-600">{subtitle}</p>
      <a href="/signup">{cta}</a>
    </section>
  )
}

export const Hero = variant("home.hero", HeroImpl)

For a section of inline JSX, wrap it in <Variant id>. A selected variant's default export replaces the children and receives the props you pass:

src/pages/home.tsx
import { Variant } from "stageboard/react"

<Variant id="home.cta" props={{ plan: "pro" }}>
  <CallToAction plan="pro" />
</Variant>

Use stable, dotted ids such as <area>.<thing>. When process.env.NODE_ENV is "production", variant() returns the original component and <Variant> renders its children, so nothing of stageboard runs in production. See React API.

Write a variant file

Each alternative is one file next to the original, named <component>.<slug>.variant.tsx. It exports variant metadata and a default component with the same props:

src/components/hero.split.variant.tsx
import type { HeroProps } from "./hero"

export const variant = {
  of: "home.hero",
  label: "Split with panel",
  description: "Left-aligned copy with a tinted product panel on the right.",
}

export default function HeroSplit({ title, subtitle, cta }: HeroProps) {
  return (
    <section className="mx-auto grid max-w-5xl grid-cols-2 items-center gap-12 px-6 py-24">
      <div>
        <h1 className="text-5xl font-semibold">{title}</h1>
        <p className="mt-4 text-lg text-zinc-600">{subtitle}</p>
        <a href="/signup">{cta}</a>
      </div>
      <div className="aspect-[4/3] rounded-2xl bg-zinc-100" />
    </section>
  )
}
FieldTypeDefaultWhat it does
ofstringrequiredThe id passed to variant() or <Variant>. Files without it are skipped.
labelstringthe keyName shown in the variant menu.
descriptionstringnoneOne sentence on what is different.
jobstringnoneSet by an agent to the request it was made for.

The variant's key is its file name without .variant.tsx: hero.split.variant.tsx has the key hero.split. stageboard reads the variant object from the source text, so keep its values as plain string literals.

The registry file

stageboard finds variant files with the variantGlobs option (default **/*.variant.{tsx,jsx,ts,js}) and writes a registry module that imports them all. It is stageboard.variants.ts next to your config, or the path in the variantsFile option (see Configuration).

stageboard.variants.ts
// Generated by stageboard. Do not edit; it is rewritten whenever *.variant.tsx files change.
import { registerVariants } from "stageboard/react"
import * as v0 from "./src/components/hero.split.variant"

registerVariants([
  { file: "src/components/hero.split.variant.tsx", module: v0 },
])

npx stageboard dev rewrites it whenever a variant file is added, changed or removed. To regenerate it by hand, run:

npx stageboard variants

Commit the file: stageboard's own frames load it automatically, and your app imports it so the in-app devtools can switch variants too.

Switch variants on the live screen

Open a screen on its own and choose Variants in the toolbar; the count shows how many variants the screen has. Every swappable part is outlined in yellow, labelled with its id and the variant it shows. Click an outline to open its menu: Original is what's in the code today, followed by every variant with its description. Pick one and the screen re-renders with it. The flow viewer has the same Variants button.

Variants that an agent is still writing are counted in the menu and appear as soon as their files exist. Ask for more variants in the menu opens a note to request more alternatives of that part. You can also ask for variants from any annotation by choosing Variants instead of Edit.

Your choice is kept by the dev server and in the browser, and applies to every screen that renders that id. It never changes production. To keep a variant for good, pick it and press Adopt as original in the menu (or in the in-app inspector). An agent ports the variant into the original component, keeps the variant() boundary and its id, and deletes the variant file; its pin shows Adopting, then Adopted. To drop a variant, delete its file.

To pin a variant in one story, set parameters.variants:

src/components/hero.stories.tsx
export const Split: Story<typeof Hero> = { parameters: { variants: { "home.hero": "hero.split" } } }

Screenshot a variant

shot takes --variant <id>=<key>, once per boundary:

npx stageboard shot marketing-hero--default --variant home.hero=hero.split --width 1440 --out /tmp/hero-split.png

Agents compare all variants of one boundary with the MCP tool compare_variants, list them with list_variants, and switch the one you see with select_variant. See CLI and MCP tools.

On this page