Skip to main content
design systems

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.


5 min read

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 / onOpenChange for open state
  • value / defaultValue / onValueChange for value state
  • disabled (never isDisabled)
  • asChild for polymorphic rendering (instead of as or component)

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.