windo/Concepts/Variants
ConceptsVariants

Variants

A variant is a named, pre-baked set of props — one click and the stage snaps to it. Where the Controls editor is for exploring a component freehand, variants are for pinning the shapes that matter: the five button emphases, the success and danger badges, the empty state. They turn a windo into a gallery of the configurations worth remembering.

The shape

variants is an array on the windo definition. Each entry is a label and a partial prop patch — never the full props, only what differs from the defaults.

TSXbadge.windo.tsx
variants: [
{ label: 'neutral', props: { variant: 'neutral', children: 'Draft' } },
{ label: 'brand', props: { variant: 'brand', children: 'New' } },
{ label: 'success · dot', props: { variant: 'success', dot: true, children: 'Active' } },
{ label: 'warning · dot', props: { variant: 'warning', dot: true, children: 'Pending' } },
{ label: 'danger', props: { variant: 'danger', children: 'Failed' } },
]

Each label becomes a button in the gallery; clicking it applies that variant's props and re-renders the stage. The label is free text — 'success · dot' reads better in a gallery than 'success' when the variant also flips the dot. Use it to describe the intent of the configuration, not just the prop value.

How a patch merges

A variant is not a snapshot of the whole component — it's a diff applied on top of defaultProps. When you click a variant, windo takes the defaults and overlays the variant's props; any key the variant omits keeps its default value.

That's why the button example only writes variant and children per entry — size, loading, and disabled are inherited from the defaults untouched.

TSXbutton.windo.tsx
defaultProps: {
variant: 'primary',
size: 'md',
loading: false,
disabled: false,
children: 'Create project',
},
variants: [
// size: 'md' and the two booleans come straight from defaultProps
{ label: 'loading', props: { variant: 'primary', loading: true, children: 'Saving…' } },
{ label: 'disabled', props: { variant: 'primary', disabled: true, children: 'Disabled' } },
],

Because the merge is a plain overlay, setting a prop to undefined in a variant is meaningful: it clears whatever the defaults provided for that slot. The Card windo uses this to show the same card with and without its media region — the media default is a JSX block, and the bare variants hand back media: undefined to drop it.

TSXcard.windo.tsx
defaultProps: {
variant: 'elevated',
title: 'Atlas redesign',
media,    // a JSX block
actions: footer,
},
variants: [
{ label: 'elevated', props: { variant: 'elevated', media: undefined } },   // drops the media
{ label: 'outlined', props: { variant: 'outlined', media: undefined } },
{ label: 'with media', props: { variant: 'elevated', media, actions: undefined } },
],

JSX in a variant

A variant patch is ordinary data, so props can hold real React nodes — an icon, a media block, a footer. The Button windo includes a "with icon" variant whose patch carries an actual element, and it renders exactly as it would in your app.

TSXbutton.windo.tsx
variants: [
{ label: 'with icon', props: { variant: 'primary', iconLeft: <MIcon d={MPATHS.plus} size={15} />, children: 'New project' } },
]

This is something the Controls editor can't express. The editor speaks JSON, so it reaches the props your zod schema describes — strings, numbers, booleans, enums. Anything that isn't serialisable — a node, a function, a ref — lives only in defaultProps and in variants. Variants are the typed escape hatch for showing those shapes off.

Variants that read the context

A static array covers the common case, but variants also accepts a function — ctx => [...] — when the gallery should depend on the environment. The most useful payoff is tailoring the set to the canvas: drop a wide showcase variant on a phone, add one only when the shared theme calls for it. Read what you need off ctx and return the array.

TSXbutton.windo.tsx
variants: (ctx) => {
const base = [
  { label: 'loading', props: { variant: 'primary', loading: true, children: 'Saving…' } },
  { label: 'disabled', props: { variant: 'primary', disabled: true, children: 'Disabled' } },
]
// a roomy side-by-side row only makes sense off mobile
if (ctx.viewport.name === 'mobile') return base
return [...base, { label: 'showcase', props: { variant: 'primary', children: 'All emphases' } }]
}

The resolver runs against a snapshot of ctx — viewport, color scheme, shared state — so the same reads available elsewhere are available here.

Variants vs. the Controls editor

They answer different questions. The editor is open-ended: drag a number, type a label, flip a boolean, watch it react. Variants are curated: here are the configurations worth bookmarking, applied in one click and identical every time.

Reach forWhenWhy
VariantsThe handful of canonical statesReviewers and teammates see the same shapes every time — no fiddling, no “what props did you set?”
Controls editorExploring or debugging a specific valueFreehand, JSON-driven, validated by your schema — but only reaches serialisable props.

In practice they pair up. Define variants for the states that matter, then drop into the editor to nudge one of them. A variant gets you to the right starting point; the editor takes it from there.

Where next

  • Configurable props — the zod-validated JSON props behind the Controls editor.
  • State & actions — drive the behaviour a variant can't, with toolbar buttons and stage triggers.
  • windo() — the full definition reference, including the variants field.