Agent docs

14 · Prop vocabulary

The words a component prop uses to describe how a thing looks or what state it is in.

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:

  • color selects the shared interactive palette, the eight-way SemanticColor (internal/variants.ts:2-10). It is the open, cross-component choice: a Button's color (button/button.types.ts:40), a Chip's color (chip/chip.types.ts:23). Reach for color when the value is one of the eight house colors and the caller is choosing freely.
  • tone selects a closed, content-specific semantic treatment, a small fixed set that belongs to one component's domain. AlertTone is customer-facing severity, info | success | warning | error | neutral (alert/alert.types.ts:5); FieldChipTone is 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.
  • kind selects a domain STATUS. StatusKind is the fixed house state vocabulary ok | warn | err | info | review | neutral (status/status.types.ts:11), matching the status canon (doc 3). kind says what a record IS doing; it is never a free color.
  • variant selects CONSTRUCTION, how the same thing is built. ButtonVariant is solid | outline | ghost | link | text (button/button.types.ts:9); ChipVariant its own set. Variant changes the frame, not the meaning.
  • shade adds 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 of variant, not a third color.
  • size selects GEOMETRY, the component's scale. Always means scale, but the allowed range is intentionally bounded per component: Button is xs-xl (button/button.types.ts:10), Select sm-lg (select/select.types.ts:7), StatusPill sm | 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:

AxisTypeQuestion it answersExamples
colorSemanticColor (internal/variants.ts:2-10)which house colorButton, Chip, StatTile, Spinner (+"current")
toneclosed per-component unionwhich content-specific treatmentAlert (alert.types.ts:5), FieldChip (field-chip.tsx:6)
kindStatusKind (status.types.ts:11)which domain statusStatusPill, StatusDot
variantper-component unionhow it is constructedButton (button.types.ts:9), Chip, StatusPill (status.types.ts:14)
shadeboolean modifieremphasis without changing constructionButton (button.types.ts:63)
sizebounded per-component scalegeometryButton 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 when tone was 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 through SURFACE[color].text (stat-tile/stat-tile.tsx), and toneClassName is the escape hatch for a raw class, with color winning. A consumer choosing a house color now reaches for color, 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 kind from the fixed six (status/status.types.ts:11), never a free color, 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 kind vocabulary and the three small-tag species; this doc owns why kind is distinct from color and tone.
  • 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.
@versable-git/ui · reference, canon, and method, read in place