Field Notes

Component Scaffolding

The workflow for scaffolding a new design system component — from dimension decisions through file generation to visual iteration.

Scaffolding a component follows a fixed sequence: decide dimensions, define types, generate files, add ARIA, iterate visually, then test.

Step 1: Decide Dimensions

Use the Component API dimension vocabulary. Ask these questions:

QuestionIf yes →Dimension
Is it interactive?Add state + emphasisstate, emphasis
Does it convey meaning?Add sentimentsentiment
Does it have size variants?Add sizesize

Every component gets a structure dimension implicitly — the fixed skeleton of border radius, font family, and gap values. This is never exposed as a prop.

Step 2: Define Types

Create a types file with union types for each dimension:

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

Rules:

  • Only include types for dimensions the component actually uses
  • Use the exact canonical mode values
  • Never use 'default', 'small', 'large', or numeric steps for size

Step 3: Generate Files

FilePurpose
component.tsxReact component with props interface
component.types.tsType unions for every dimension
component.css (or .scss)Styles using CSS custom properties
component.stories.tsxStorybook stories covering all dimensions
component.test.tsxUnit tests for inputs, outputs, ARIA
index.tsPublic barrel export

Component file pattern

import type { ButtonEmphasis, ButtonSentiment, ButtonSize, ButtonState } from './button.types';

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

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

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

Step 4: Add ARIA

Choose ARIA attributes based on the component type:

Component typeroleKey attributes
Button / actionbuttonaria-label, aria-disabled, aria-pressed (toggle)
Linklinkaria-label, aria-current (active nav)
Status displaystatusaria-live="polite", aria-label
Alert / notificationalertaria-live="assertive", aria-atomic="true"
Form input— (native)aria-invalid, aria-describedby, aria-required
Dialog / modaldialogaria-modal="true", aria-labelledby
Tabtabaria-selected, aria-controls
Progressprogressbararia-valuenow, aria-valuemin, aria-valuemax

Always include:

  • aria-label or visible label association
  • aria-disabled on disabled interactive elements
  • Focus management with :focus-visible styling

Step 5: Visual Iteration

After scaffolding, iterate on token values visually in Storybook or a dev environment:

  1. Open the component with default token values
  2. Adjust token values — background, foreground, border colors per dimension
  3. Test emphasis/sentiment combinations visually
  4. Verify size scale feels proportional
  5. Check state transitions (hover, active, disabled)
  6. Validate contrast ratios meet WCAG AA (4.5:1 text, 3:1 UI)

Visual Iteration Checklist

  • All emphasis levels render with distinct visual weight
  • All sentiment colors are distinguishable (not just by hue — check luminance)
  • Size scale maintains visual proportion (text, padding, icon sizing)
  • Disabled state is visually distinct but not invisible
  • Hover/active states provide clear feedback
  • Focus ring is visible on all background colors
  • Dark and light themes both work
  • Reduced motion preference is respected
  • Touch targets meet WCAG 2.2 minimum (44×44px for md and above)

Step 6: Test

See Testing Strategy for the three-layer approach.

Size Scale Reference

ModeHeightPadding XGapTouch target
xs24px6px4pxBelow minimum — use sparingly
sm32px10px6pxBelow minimum — dense interfaces
md40px14px8pxMeets 44px with padding — default
lg48px18px10pxMeets WCAG 2.2 minimum
xl56px22px12pxHero / prominent CTAs

Default is always 'md'.

See Also

On this page