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.
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.
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.
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.
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 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 for | When | Why |
|---|---|---|
| Variants | The handful of canonical states | Reviewers and teammates see the same shapes every time — no fiddling, no “what props did you set?” |
| Controls editor | Exploring or debugging a specific value | Freehand, 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
variantsfield.