The words a component prop uses to describe how a thing looks or what state it is in. Six axes, each with one meaning, and one shared color type underneath them. Kit sources: packages/ui/src/internal/variants.ts, and the *.types.ts of every component that takes a visual prop. This doc is law: it says what each axis MEANS and why the six stay distinct; it never restates a component's contract (read ../../packages/ui/docs/ for props).
Audience: an agent (or person) naming a new visual prop, or deciding which existing axis a value belongs to.
§A Principles and mental models#
1. Six visual axes, each with exactly one meaning.
color, tone, kind, variant, shade, and size are not competing names for one idea, and uniforming them would erase useful distinctions. Each answers a different question:
colorselects the shared interactive palette, the eight-waySemanticColor(internal/variants.ts:2-10). It is the open, cross-component choice: a Button'scolor(button/button.types.ts:40), a Chip'scolor(chip/chip.types.ts:23). Reach forcolorwhen the value is one of the eight house colors and the caller is choosing freely.toneselects a closed, content-specific semantic treatment, a small fixed set that belongs to one component's domain.AlertToneis customer-facing severity,info | success | warning | error | neutral(alert/alert.types.ts:5);FieldChipToneis an identity category,field | file(field-chip/field-chip.tsx:6). A tone's vocabulary is deliberately not the palette; it is the meanings that component draws.kindselects a domain STATUS.StatusKindis the fixed house state vocabularyok | warn | err | info | review | neutral(status/status.types.ts:11), matching the status canon (doc 3).kindsays what a record IS doing; it is never a free color.variantselects CONSTRUCTION, how the same thing is built.ButtonVariantissolid | outline | ghost | link | text(button/button.types.ts:9);ChipVariantits own set. Variant changes the frame, not the meaning.shadeadds emphasis WITHOUT changing construction. On Button it is a boolean (button/button.types.ts:63): a quiet variant washed in its own color, deepening on hover. It is a modifier on top ofvariant, not a third color.sizeselects GEOMETRY, the component's scale. Always means scale, but the allowed range is intentionally bounded per component: Button isxs-xl(button/button.types.ts:10), Selectsm-lg(select/select.types.ts:7), StatusPillsm | md(status/status.types.ts:13). Do not widen every component to one global size union for type symmetry; the bounds are the design.
2. One color palette underneath, never a per-component color union.
SemanticColor (internal/variants.ts:2-10) is the single palette every color-accepting prop draws from. ChipColor is an alias of it, not a copy (chip/chip.types.ts:8); StatTile.color is SemanticColor (stat-tile/stat-tile.types.ts:33). The one sanctioned extension is SpinnerColor = SemanticColor | "current" (spinner/spinner.types.ts:7-8): a spinner often inherits the surrounding text color, so "current" is a real, justified addition, not a divergence. A new color prop is SemanticColor (or the spinner extension); it never re-lists the eight members by hand.
3. A raw class escape hatch is named for what it is, and the semantic prop wins.
Where a component still accepts a raw Tailwind class for a color it cannot yet express semantically, that prop is named toneClassName (or similar *ClassName), the typed semantic prop is color, and color wins over the raw hatch when both are set (stat-tile/stat-tile.types.ts:33,36). The raw hatch is a bridge to a future major version, not a parallel API; a plain tone?: string that swallows any class is the anti-pattern this rule retired.
4. Presets are shorthands, never gates.
Everything a preset sets can be set by hand. Plural presets means a composable bundle (Button, Chip: one or an ordered array); singular preset means one exclusive mode (Select, ConfirmModal). Keep both conventions; do not collapse them.
§B Presets and where each axis appears#
The shared type and the axis-to-component map, so a new prop lands on the right axis:
| Axis | Type | Question it answers | Examples |
|---|---|---|---|
color | SemanticColor (internal/variants.ts:2-10) | which house color | Button, Chip, StatTile, Spinner (+"current") |
tone | closed per-component union | which content-specific treatment | Alert (alert.types.ts:5), FieldChip (field-chip.tsx:6) |
kind | StatusKind (status.types.ts:11) | which domain status | StatusPill, StatusDot |
variant | per-component union | how it is constructed | Button (button.types.ts:9), Chip, StatusPill (status.types.ts:14) |
shade | boolean modifier | emphasis without changing construction | Button (button.types.ts:63) |
size | bounded per-component scale | geometry | Button xs-xl, Select sm-lg, StatusPill sm/md |
The raw-class hatch (toneClassName) sits beside color on the one component that still needs it (StatTile), deprecated in favor of the semantic prop.
§C Cases, through the apps#
- StatTile picks the semantic axis over a raw class. Forge's dashboard passed
tone="warning"to StatTile whentonewas a raw class slot, which type-checked but named no class the component read (the defect finding 1.2 caught). The fix put the value on the right axis:color="warning"maps throughSURFACE[color].text(stat-tile/stat-tile.tsx), andtoneClassNameis the escape hatch for a raw class, withcolorwinning. A consumer choosing a house color now reaches forcolor, not a Tailwind string. - Chip and Button share the palette, not a copy. Both take
color?: SemanticColor(chip/chip.types.ts:23,button/button.types.ts:40), so a color that reads on one reads on the other, and a new house color is added in one place. - Status stays a closed kind, not a color. A row's state is
kindfrom the fixed six (status/status.types.ts:11), never a freecolor, because the status canon (doc 3) fixes that vocabulary and a badge's job is to mean, not to decorate.
Travels with#
- Doc 3 (status language) owns the
kindvocabulary and the three small-tag species; this doc owns whykindis distinct fromcolorandtone. - The per-component contracts in
../../packages/ui/docs/say which axes each component takes; this doc says what the axes mean. - Doc 1 (foundations) owns the token layers the palette resolves against.