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:
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:
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:
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>
)
}| Field | Type | Default | What it does |
|---|---|---|---|
of | string | required | The id passed to variant() or <Variant>. Files without it are skipped. |
label | string | the key | Name shown in the variant menu. |
description | string | none | One sentence on what is different. |
job | string | none | Set 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).
// 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 variantsCommit 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:
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.pngAgents 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.