Prop grammar
Twelve rules govern the public prop vocabulary of every component, so state, configuration, selection, and slot props are spelled the same way wherever you meet them. The rules on this page are read straight from the shared grammar module — the one source the doctrine export and the grammar audit also consume — so the page can never disagree with what CI enforces.
Audit-enforced
Checkable against the emitted declarations and enforced by tools/audit-catalog.mjs — a violation fails CI, keyed by the rule id.
G1
State booleans are `is*`
State booleans are `is*`: `isDisabled`, `isSelected`, `isInvalid`, `isLoading`, `isOpen`, `isReadOnly`, `isRequired`, `isIndeterminate`. Never bare `disabled`, `checked`, `selected`, `invalid`, `loading`, `open` in a public props type — including item/option object types.
isDisabled // not `disabled`, even on an item/option object type
G4
Status tones say `error`, actions say `destructive`
Status tones say `error`, actions say `destructive`: `danger` is forbidden. Status components (Alert, Toast, Callout, Banner, Chip, Badge) use `error`; action components (Button tone, `MenuItem.isDestructive`) use `destructive`.
tone="error" (status) tone="destructive" (action) // never `danger`
G5
Selection API
Selection API: single — `selectedKey` / `defaultSelectedKey` / `onSelectionChange(key: string)`; multiple — `selectedKeys` / `defaultSelectedKeys` / `onSelectionChange(keys: string[])`; where variable, `selectionMode: 'none' | 'single' | 'multiple'` defaulting to `'none'`. `value`/`onValueChange` is forbidden for selection.
selectedKey={key} onSelectionChange={setKey} // not value/onValueChange
G6
Accessible name is `'aria-label'`
Accessible name is `'aria-label'` (quoted string prop) in the public API, mapped internally to RN `accessibilityLabel`. It is required (not optional) on components with no visible text (`Table`, `TreeView`, charts, `CommandMenu`, `FileUploader`, `QRCode` uses `label`). `IconButton` uses required `label`. Public `accessibilityLabel?:` props are forbidden.
"aria-label" // public prop; `accessibilityLabel` stays internal-only
G11
Tokens only
Tokens only: color-typed props are `ThemeColor`, spacing props are `ThemeSpacing`; string color literals in public APIs are forbidden except where the value is data, not style (ColorPicker/GradientPicker values). Enforced by the existing NFR-T1/T2 audit rules.
color: ThemeColor gap: ThemeSpacing // not "#3b82f6" / 12
Conventions
Design conventions reviewers uphold. Not mechanically checked, but held to the same standard everywhere.
G2
Visual configuration is a bare adjective
Visual configuration is a bare adjective, never `is*`: `slim`, `fullWidth`, `compact`, `dot`, `verified`, `arrow`. Test: if the value changes through user interaction during the component’s lifetime it is state (G1); if it is set once by the author it is configuration (G2).
slim fullWidth verified // set once by the author, so no `is*`
G3
Three orthogonal style axes
Three orthogonal style axes: `variant` = structural style (`primary|secondary|tertiary|link`, `subtle|solid|outline|modern`); `tone` = semantic color intent (`brand|gray|destructive` for actions; `error|warning|success|info|neutral` for status); `size` = t-shirt subset of `xs|sm|md|lg|xl`. Never a single crossed axis (Untitled UI’s `color="secondary-destructive"` is expressed here as `variant="secondary" tone="destructive"`).
variant="secondary" tone="destructive" // not color="secondary-destructive"
G7
Slot naming
Slot naming: `iconLeading` / `iconTrailing` when the slot is specifically an icon (Button, ButtonGroup items); `leading` / `trailing` when the slot accepts arbitrary `ReactNode` (Badge, ListItem, Select/Tag options); bare `icon` only where a single unambiguous icon slot exists (MenuItem, FeaturedIcon child).
iconLeading (icon-only) leading (any ReactNode) icon (single slot)
G8
Collections are data-driven arrays
Collections are data-driven arrays — `items` / `options` / `columns` of typed objects with `value` (not `id`), `label`, optional `isDisabled` — never compound-children APIs (`X.Item`, `X.Panel`). Rationale in §E (collections deviation).
items={[{ value, label, isDisabled }]} // not <X.Item>…</X.Item>
G9
Field text trio
Field text trio: `label`, `description`, `errorMessage` (+ `isInvalid`), never Untitled UI’s `hint`. This follows the React Aria substrate more closely than Untitled UI itself does.
label / description / errorMessage // not a single `hint`
G10
Events and control
Events and control: RN `onPress` (never `onClick`); controlled/uncontrolled pairs are `value` / `defaultValue` / `onChange` via `useControllableState`; overlays use `isOpen` / `defaultOpen` / `onOpenChange`.
onPress • value/defaultValue/onChange • isOpen/defaultOpen/onOpenChange
G12
Sizes default `md`
Sizes default `md` (not Untitled UI’s `sm`) — 44pt touch-target floor on mobile.
size="md" // default; UUI defaults to `sm`