Field Notes

Component API

How to define the consumer-facing contract of a design system component — inputs, types, defaults, and slots.

A component API has exactly four categories of input. Button is the canonical example — every other component is a subset or extension of this shape.

Dimension inputs  →  which token collection mode to activate
Content inputs    →  text and values the component renders
Slot inputs       →  optional content areas (icons, badges, avatars)
Event outputs     →  interactions communicated back to the parent

Dimensions compose independently. Changing sentiment does not affect size. The valid state space is their product, not their sum — a Button with 3 emphasis × 6 sentiment × 5 size modes has 90 working combinations automatically, because modes resolve at runtime rather than being pre-combined.

Additive (wrong):       3 + 6 + 5 = 14 things to define
Multiplicative (right): 3 × 6 × 5 = 90 combinations, all free

Adding one new sentiment mode costs one token value — not a new variant branch.

The Three Rules

Enums, not booleans. Every dimension input is a string enum, even with only two options today. A boolean can never grow without a breaking change. Booleans also create parallel state channels — if state and loading can both be set, what happens when state='disabled' and loading=true? Enums eliminate the ambiguity.

Every input has a default. A bare <Button /> must render sensibly without any attributes. The default is always the most neutral, common-case value.

Names describe the dimension, not the visual. emphasis: 'high' survives a redesign. isPrimary: true does not. Name the concept, not the current treatment.

Canonical Example: Button

Button is the reference point. Every other component API is derived by asking: which of these dimensions does this component need, and what content/slots does it add?

Types

type ButtonState     = 'rest' | 'hover' | 'active' | 'disabled' | 'resolving' | 'pending';
type ButtonEmphasis  = 'high' | 'medium' | 'low';
type ButtonSentiment = 'neutral' | 'warning' | 'highlight' | 'new' | 'success' | 'error';
type ButtonSize      = 'xs' | 'sm' | 'md' | 'lg' | 'xl';

React Component

interface ButtonProps {
  state?: ButtonState;
  emphasis?: ButtonEmphasis;
  sentiment?: ButtonSentiment;
  size?: ButtonSize;
  leadingIcon?: React.ReactNode;
  trailingIcon?: React.ReactNode;
  onClick?: (event: React.MouseEvent) => void;
  children: React.ReactNode;
}

function Button({
  state = 'rest',
  emphasis = 'high',
  sentiment = 'neutral',
  size = 'md',
  leadingIcon,
  trailingIcon,
  onClick,
  children,
}: ButtonProps) {
  const isDisabled = state === 'disabled';

  return (
    <button
      data-state={state}
      data-emphasis={emphasis}
      data-sentiment={sentiment}
      data-size={size}
      disabled={isDisabled}
      onClick={isDisabled ? undefined : onClick}
    >
      {leadingIcon && (
        <span className="button__prefix" aria-hidden="true">{leadingIcon}</span>
      )}
      <span className="button__label">{children}</span>
      {trailingIcon && (
        <span className="button__suffix" aria-hidden="true">{trailingIcon}</span>
      )}
    </button>
  );
}

Dimension Vocabulary

Use these exact values. Do not invent synonyms.

DimensionStandard modesInclude when
staterest hover active selected disabled resolving pendingComponent is interactive
emphasishigh medium lowMultiple visual weights exist
sentimentneutral warning highlight new success errorConveys communicative meaning
sizexs sm md lg xlHas size variants
structure(no input — always applied)Never expose as a prop

State + Emphasis are paired. Interactive components almost always need both.

Loading is a state mode. state='resolving' — not a separate loading boolean.

Slot Patterns

Same component type every time → conditional rendering

The icon slot is either present or absent. Conditional rendering prevents ghost padding from empty wrappers.

{leadingIcon && (
  <span className="button__prefix" aria-hidden="true">{leadingIcon}</span>
)}

Different component types → polymorphic slot

interface CardProps {
  leading?: React.ReactNode;  // Icon, Avatar, or Checkbox
  trailing?: React.ReactNode;
}
Slot namePositionPattern
leadingIcon / prefixBefore labelConditional — Icon only
trailingIcon / suffixAfter labelConditional — Icon only
leadingStart of layoutPolymorphic — mixed types
trailingEnd of layoutPolymorphic — mixed types
header / footerTop / bottomPolymorphic — mixed types

Extrapolating to Other Components

Start from Button and ask: which dimensions does this component need, what content does it render, what slots does it have, and does the layer structure ever change?

If the layer structure changes between options → that is a variant (separate component), not a mode. Avatar with image vs initials vs icon has different child layers — three variants. Button with different sentiments has the same layers — one component, multiple modes.

ComponentDimensionsDroppedAdditions
Badgesentiment, size (xs–lg)state, emphasistext: string
IconButtonstate, emphasis, sentiment, sizeIcon is required content, no label
Chipstate, emphasis, sentiment, sizecloseable?: boolean
Inputstate, sizeemphasislabel, placeholder, value; leading/trailing slots
Alertsentimentstate, emphasis, sizetitle, description; leading slot; actions slot
Selectstate, sizeemphasisComposite — parent owns dimensions, SelectItem owns value

For composite components: the parent owns all dimension inputs and propagates them to children. Children own only their content inputs.

Decision Tree

New condition (loading, pending, selected)?
  → Mode on `state` — not a new input

New visual weight (outlined, ghost, subtle)?
  → Mode on `emphasis` — not a new input

Layer structure changes between options?
  → Variant → separate component

Content (label, text, placeholder, value)?
  → String prop with default ''

Slot presence (icon, close button)?
  → Optional React.ReactNode prop

None of the above?
  → New enum input. Name the dimension. Provide a default.

Audit Checklist

CheckViolationFix
Type is boolean for a dimensionRaw booleanChange to string union type
No defaultMissing defaultAdd — most neutral/common value
Name is visual (isPrimary, isGhost)Visual namingRename to dimension (emphasis)
Mode values non-standard ('small', '1')Wrong modesUse canonical vocabulary above
Dimension on child that parent controlsMisplaced ownershipMove to parent, propagate down
loading as separate booleanParallel channelMerge into state='resolving'

See Also

On this page