The Component API Test: Can Someone Guess the Next Prop?
A practical measure of design system quality: if you know the Button API, can you guess the Input API? Consistency is the feature.
If a developer uses your Button component and then reaches for your Input, they should already know half the API. Same size values. Same variant names. Same approach to disabled. If they have to read the docs every time they switch components, your design system has an API consistency problem — not a documentation problem. The components are inconsistent, and documentation cannot fix that.
The Test
Sit down with someone who has used one component in your system. Hand them a new component and ask them to write usage without looking at docs.
If they reach for size="lg" on the new component and your system uses size="large", you failed. If they pass onChange and you named it onValueChange, you failed. If they try disabled and it only exists as isDisabled, you failed.
The principle of least surprise says: names that behave similarly should look similar. The component API is a language. Every inconsistency in that language is a tax on every developer who uses it.
What Radix UI Gets Right
Radix UI passes the test. Across every primitive — Dialog, Select, Checkbox, DropdownMenu — the API follows predictable patterns:
open/defaultOpen/onOpenChangefor open statevalue/defaultValue/onValueChangefor value statedisabled(neverisDisabled)asChildfor polymorphic rendering (instead ofasorcomponent)
Once you learn Dialog.Trigger, you understand DropdownMenu.Trigger. Once you see Select.Value, you recognize the pattern in RadioGroup.Item. The system is internally consistent, and that consistency is the documentation.
What Inconsistent Libraries Do
Compare the experience of stitching together random component libraries. One uses size="sm | md | lg", another uses size="small | medium | large", a third uses size={1 | 2 | 3}. One calls it variant, another calls it appearance, another kind. You spend time translating between dialects rather than building product.
// Inconsistent — every component is its own dialect
<Button size="sm" appearance="primary" />
<Input size="small" variant="outlined" />
<Badge kind="success" scale="compact" />
// Consistent — one vocabulary
<Button size="sm" variant="primary" />
<Input size="sm" variant="outlined" />
<Badge size="sm" variant="success" />
The second set means someone using Badge for the first time can guess the right props without a docs lookup. That is the feature.
Naming Conventions Worth Standardizing
Size: Use "xs" | "sm" | "md" | "lg" | "xl" and stick to it across every component that accepts size. Never mix "small" and "sm" in the same system.
Variant: Use variant for visual style — "default" | "outline" | "ghost" | "destructive". Reserve color for actual color overrides, not named styles.
State props: disabled, loading, readOnly — no is prefix. HTML does not use isDisabled. React components should not either.
Event handlers: onChange for value changes, onSelect for selection, onOpenChange(open: boolean) for toggleable state. Avoid onToggle, onCheck, onActivate — they fragment the vocabulary.
Content: children for slotted content, label for accessible labels when children is taken. Never text, title (unless it means the HTML title attribute), or copy.
Composition Patterns
Consistent composition matters as much as consistent naming. If Button accepts leftIcon and rightIcon as props, Input should accept leftAddon and rightAddon. Better: adopt the slot pattern and use children composition everywhere.
// Prop-per-slot — does not scale
<Input leftIcon={<SearchIcon />} rightIcon={<ClearIcon />} />
// Slot composition — scales to any number of slots
<Input>
<Input.Slot side="left"><SearchIcon /></Input.Slot>
<Input.Field placeholder="Search..." />
<Input.Slot side="right"><ClearIcon /></Input.Slot>
</Input>
The slot pattern means you never need leftIcon, rightIcon, leftAddon, prefixText, or startAdornment as one-off props. The pattern handles everything without API proliferation.
Enforcing Consistency
The design system's TypeScript types are its contract. Define shared prop types in a central location and reuse them:
// design-system/types.ts
export type Size = "xs" | "sm" | "md" | "lg" | "xl";
export type Variant = "default" | "outline" | "ghost" | "destructive";
// button.tsx
import { Size, Variant } from "../types";
interface ButtonProps { size?: Size; variant?: Variant; }
// input.tsx
import { Size, Variant } from "../types";
interface InputProps { size?: Size; variant?: Variant; }
When Size is a shared type, a rename is one change. When it is a string literal duplicated across 30 components, it is 30 changes and inevitable drift. The type is the contract. Keep it in one place.