Deep Dive: Composers

Why Composers Exist

Primitives give us atoms, compounds give us molecules — but product interfaces demand more. A composer is where a design system stops bundling parts and starts orchestrating interaction and state.

Think of a modal, a toolbar, pagination, or a form fieldset. These aren’t just bundles of primitives — they coordinate:

  • Multiple states (open/closed, selected/unselected, error/valid).
  • Multiple flows (keyboard vs mouse, small vs large screen, logged in vs logged out).
  • Multiple roles (what happens to focus, what gets announced to a screen reader, what rules apply when contents vary).

Composers exist because user interactions don’t stop at a single element — they span across elements.

Characteristics of Composers

  • Orchestration: manage focus, context, and state for child primitives/compounds.
  • Slotting: expose defined areas (header, body, footer, actions) for flexible composition.
  • Variation by pattern, not prop: handle families of behavior (e.g., ellipses in pagination) rather than a Boolean soup of configuration.
  • Context Providers: share state between sub-parts without forcing prop-drilling.

Examples of Composers

  • Modal: orchestrates open/close, traps focus, provides slots for header/body/footer.
  • Form Field: orchestrates label, input, error messaging across multiple input types.
  • Toolbar / Filter Bar: orchestrates a dynamic set of actions, priorities, and overflow menus.
  • Pagination: orchestrates page numbers, overflow ellipses, compact vs full modes.
  • Rich Text Editor: orchestrates schema, commands, plugins, and UI slots.

The Work of the System at the Composer Layer

1. Orchestration

  • Control state transitions (modal open → trap focus → restore focus on close).
  • Govern keyboard interaction models (arrow key navigation in toolbars, tab order in forms).
  • Provide context for sub-parts (form state, toolbar action registry).

2. Variation by Pattern

  • Instead of adding a prop for every variant, encode structural patterns.
  • Example: Pagination doesn’t expose showEllipses: boolean; it defines a policy for when ellipses appear.

3. Slots for Composition

  • Provide places for product-specific content without breaking orchestration.
  • Example: Modal slots for header/body/footer let teams add what they need while the system enforces a11y and focus rules.

Pitfalls of Composers

  1. Prop Explosion as a Lazy Shortcut
    • Composers often start with props for each variation: hasCloseButton, showFooter, isInline, isSticky.
    • Guardrail: encode variations as structural patterns, not toggles.
  2. Leaking Internal State
    • If a form composer exposes internal validation state poorly, teams may hack around it.
    • Guardrail: provide a clean context/hook API for internal orchestration.
  3. Breaking Accessibility in the Orchestration
    • Example: a modal that doesn’t trap focus or a toolbar without roving tabindex.
    • Guardrail: accessibility rules must be first-class orchestration, not optional add-ons.
  4. Overgeneralization
    • Composers aren’t universal solutions. A “SuperModal” that tries to handle every drawer/alert/dialog variant will be brittle.
    • Guardrail: scope composers to a pattern family, not the entire design problem space.

The Problem: Without Composers

Before we see composers in action, let's understand what happens without them. Consider building modals across a large application:

import { useState } from 'react';
import { BadModal } from './BadModal';

export default function App() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div style={{ padding: '20px', fontFamily: 'system-ui' }}>
      <h2>❌ Problems Without Composers</h2>
      
      <button 
        onClick={() => setShowModal(true)}
        style={{
          padding: '10px 20px',
          backgroundColor: '#dc3545',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer'
        }}
      >
        Open Problematic Modal
      </button>

      <BadModal isOpen={showModal} onClose={() => setShowModal(false)}>
        <h3>Issues with this approach:</h3>
        <ul style={{ textAlign: 'left', paddingLeft: '20px' }}>
          <li>🐛 Clicking content closes modal (missing stopPropagation)</li>
          <li>♿ No focus trap or ARIA attributes</li>
          <li>🔄 Every modal reimplements escape key handling</li>
          <li>📱 No responsive behavior considerations</li>
          <li>🎨 Inconsistent styling across modals</li>
          <li>🧪 Hard to test - logic scattered everywhere</li>
        </ul>
        
        <button 
          onClick={() => setShowModal(false)}
          style={{
            marginTop: '16px',
            padding: '8px 16px',
            backgroundColor: '#6c757d',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
          }}
        >
          Close
        </button>
      </BadModal>
      
      <div style={{ 
        marginTop: '20px', 
        padding: '16px', 
        backgroundColor: '#f8d7da', 
        border: '1px solid #f5c6cb',
        borderRadius: '4px',
        color: '#721c24'
      }}>
        <strong>Result:</strong> Every team builds modals differently, creating inconsistent UX, 
        accessibility gaps, and maintenance nightmares. This is exactly what composers solve.
      </div>
    </div>
  );
}

The Solution: Modal Composer

Now let's see how a composer centralizes this complexity into a reliable, reusable orchestration layer:

import { useState } from 'react';
import { Modal } from './Modal';
import { Button } from './Button';

export default function App() {
  const [showBasic, setShowBasic] = useState(false);
  const [showConfirm, setShowConfirm] = useState(false);
  const [showForm, setShowForm] = useState(false);

  return (
    <div style={{ padding: '20px', fontFamily: 'system-ui' }}>
      <h2>Modal Composer Examples</h2>
      <p>Modals orchestrate focus, escape handling, and overlay behavior while providing slots for flexible content.</p>
      
      <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
        <Button onClick={() => setShowBasic(true)}>
          Basic Modal
        </Button>
        <Button onClick={() => setShowConfirm(true)} variant="danger">
          Confirm Dialog
        </Button>
        <Button onClick={() => setShowForm(true)} variant="secondary">
          Form Modal
        </Button>
      </div>

      {/* Basic Modal */}
      <Modal open={showBasic} onClose={() => setShowBasic(false)}>
        <Modal.Header>Welcome!</Modal.Header>
        <Modal.Body>
          <p>This is a basic modal with header and body slots.</p>
          <p>The modal composer handles:</p>
          <ul>
            <li>✅ Focus trap and escape key</li>
            <li>✅ Overlay click to close</li>
            <li>✅ Flexible content via slots</li>
            <li>✅ Consistent styling</li>
          </ul>
        </Modal.Body>
        <Modal.Footer>
          <Button onClick={() => setShowBasic(false)}>
            Close
          </Button>
        </Modal.Footer>
      </Modal>

      {/* Confirmation Modal */}
      <Modal open={showConfirm} onClose={() => setShowConfirm(false)}>
        <Modal.Header>Confirm Action</Modal.Header>
        <Modal.Body>
          <p>Are you sure you want to delete this item? This action cannot be undone.</p>
        </Modal.Body>
        <Modal.Footer>
          <Button 
            variant="secondary" 
            onClick={() => setShowConfirm(false)}
          >
            Cancel
          </Button>
          <Button 
            variant="danger"
            onClick={() => {
              alert('Item deleted!');
              setShowConfirm(false);
            }}
          >
            Delete
          </Button>
        </Modal.Footer>
      </Modal>

      {/* Form Modal */}
      <Modal open={showForm} onClose={() => setShowForm(false)}>
        <Modal.Header>Add New Item</Modal.Header>
        <Modal.Body>
          <form style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
            <div>
              <label style={{ display: 'block', marginBottom: '4px', fontWeight: '500' }}>
                Name
              </label>
              <input 
                style={{
                  width: '100%',
                  padding: '8px 12px',
                  border: '1px solid #ccc',
                  borderRadius: '4px',
                  fontSize: '16px'
                }}
                placeholder="Enter item name"
              />
            </div>
            <div>
              <label style={{ display: 'block', marginBottom: '4px', fontWeight: '500' }}>
                Description
              </label>
              <textarea 
                style={{
                  width: '100%',
                  padding: '8px 12px',
                  border: '1px solid #ccc',
                  borderRadius: '4px',
                  fontSize: '16px',
                  minHeight: '80px',
                  resize: 'vertical'
                }}
                placeholder="Enter description"
              />
            </div>
          </form>
        </Modal.Body>
        <Modal.Footer>
          <Button 
            variant="secondary" 
            onClick={() => setShowForm(false)}
          >
            Cancel
          </Button>
          <Button onClick={() => {
            alert('Item saved!');
            setShowForm(false);
          }}>
            Save
          </Button>
        </Modal.Footer>
      </Modal>

      <div style={{ 
        marginTop: '40px', 
        padding: '16px', 
        backgroundColor: '#f8f9fa', 
        borderRadius: '4px' 
      }}>
        <h3>Composer Benefits:</h3>
        <ul style={{ margin: 0, paddingLeft: '20px' }}>
          <li>🎯 <strong>Orchestration:</strong> Handles focus, keyboard, overlay behavior</li>
          <li>🧩 <strong>Slotting:</strong> Header/Body/Footer for flexible composition</li>
          <li><strong>Accessibility:</strong> Focus trap, escape key, ARIA built-in</li>
          <li>🔄 <strong>Reusability:</strong> Same modal, different content patterns</li>
        </ul>
      </div>
    </div>
  );
}

Advanced Example: Form Field Composer

Let's see a more complex composer that demonstrates context-based orchestration, managing multiple children and coordinating validation state:

import { useState } from 'react';
import { FormField } from './FormField';

export default function App() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
    confirmPassword: ''
  });

  const emailValidator = (value: string) => {
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      return 'Please enter a valid email address';
    }
  };

  const passwordValidator = (value: string) => {
    if (value.length < 8) {
      return 'Password must be at least 8 characters';
    }
    if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
      return 'Password must contain uppercase, lowercase, and number';
    }
  };

  const confirmPasswordValidator = (value: string) => {
    if (value !== formData.password) {
      return 'Passwords do not match';
    }
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // In real implementation, you'd validate all fields
    alert('Form submitted! Check console for orchestration benefits.');
    console.log('🎯 Composer Benefits Demonstrated:');
    console.log('✅ Consistent ARIA relationships across all fields');
    console.log('✅ Centralized validation orchestration');
    console.log('✅ Context-based child coordination');
    console.log('✅ No prop drilling - children access field state via context');
    console.log('✅ Reusable validation patterns');
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'system-ui', maxWidth: '500px' }}>
      <h2>✅ Form Field Composer</h2>
      <p style={{ color: '#666', marginBottom: '30px' }}>
        This composer orchestrates validation, accessibility, and child coordination 
        through React Context. Notice how each field is self-contained yet consistent.
      </p>
      
      <form onSubmit={handleSubmit}>
        <FormField required validator={emailValidator}>
          <FormField.Label>Email Address</FormField.Label>
          <FormField.Input 
            type="email"
            placeholder="Enter your email"
            value={formData.email}
            onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
          />
          <FormField.Helper>We'll never share your email with anyone</FormField.Helper>
          <FormField.Error />
        </FormField>

        <FormField required validator={passwordValidator}>
          <FormField.Label>Password</FormField.Label>
          <FormField.Input 
            type="password"
            placeholder="Create a password"
            value={formData.password}
            onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
          />
          <FormField.Helper>Must be 8+ chars with uppercase, lowercase, and number</FormField.Helper>
          <FormField.Error />
        </FormField>

        <FormField required validator={confirmPasswordValidator}>
          <FormField.Label>Confirm Password</FormField.Label>
          <FormField.Input 
            type="password"
            placeholder="Confirm your password"
            value={formData.confirmPassword}
            onChange={(e) => setFormData(prev => ({ ...prev, confirmPassword: e.target.value }))}
          />
          <FormField.Helper>Must match your password above</FormField.Helper>
          <FormField.Error />
        </FormField>

        <button 
          type="submit"
          style={{
            padding: '12px 24px',
            backgroundColor: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            fontSize: '16px',
            marginTop: '10px'
          }}
        >
          Create Account
        </button>
      </form>
      
      <div style={{ 
        marginTop: '30px', 
        padding: '20px', 
        backgroundColor: '#d4edda', 
        border: '1px solid #c3e6cb',
        borderRadius: '4px',
        color: '#155724'
      }}>
        <h3 style={{ margin: '0 0 12px 0' }}>🎯 Composer Orchestration:</h3>
        <ul style={{ margin: 0, paddingLeft: '20px' }}>
          <li><strong>Context Coordination:</strong> Children access field state without prop drilling</li>
          <li><strong>ARIA Management:</strong> Automatic describedBy relationships</li>
          <li><strong>Validation Orchestration:</strong> Centralized error handling</li>
          <li><strong>State Synchronization:</strong> Error clearing, required indicators</li>
          <li><strong>Consistent Behavior:</strong> All fields follow same patterns</li>
        </ul>
      </div>
    </div>
  );
}

Why Composers Are Essential

Composers solve the “coordination problem” that emerges in design systems at scale. They provide governance through orchestration:

🎯 Orchestration Benefits

  • Single Source of Truth: Complex behavior lives in one place, not scattered across implementations
  • Consistent Patterns: Every modal, form field, or toolbar behaves identically
  • Accessibility by Default: ARIA relationships, focus management, and keyboard behavior built-in
  • Easier Testing: Test the composer once, trust it everywhere

🧩 Composition Benefits

  • Flexible Content: Slots allow varied content while maintaining consistent behavior
  • Context Coordination: Children access orchestrated state without prop drilling
  • Separation of Concerns: Content creators focus on content, not complex behavior
  • Reusable Patterns: Same orchestration, infinite content variations

⚖️ Governance Benefits

  • Prevents Drift: Teams can't accidentally build inconsistent versions
  • Enforces Standards: Accessibility and UX patterns are automatic
  • Reduces Maintenance: Fix behavior once, it's fixed everywhere
  • Enables Scale: New team members get consistent behavior “for free”

Case Study: OTP Composer

A one-time passcode (OTP) input is a great example of a composer: it coordinates multiple input fields, manages paste behavior, advances focus, and exposes slots for labels, separators, and errors — all while remaining brand-agnostic and token-driven.

import { OTPProvider, OTPInput, OTPField, OTPSeparator, OTPLabel, OTPError } from './index';

export default function App() {
  return (
    <div style={{ padding: 20, fontFamily: 'system-ui', maxWidth: 480 }}>
      <h2>OTP Composer</h2>
      <p style={{ color: '#666' }}>
        Headless logic + slots. Paste a 6-digit code into any field, use arrow keys, or backspace across fields.
      </p>
      <form onSubmit={(e) => { e.preventDefault(); alert('Verified'); }}>
        <OTPProvider length={6} mode="numeric" onComplete={(code) => alert('Code: ' + code)}>
          <OTPLabel>Enter the 6-digit code</OTPLabel>
          <OTPInput>
            <OTPField index={0} />
            <OTPField index={1} />
            <OTPField index={2} />
            <OTPSeparator>-</OTPSeparator>
            <OTPField index={3} />
            <OTPField index={4} />
            <OTPField index={5} />
          </OTPInput>
          <OTPError id="otp-error">&nbsp;</OTPError>
        </OTPProvider>
        <button type="submit" style={{ marginTop: 16, padding: '10px 16px', borderRadius: 6, border: '1px solid #ced4da', background: '#f8f9fa' }}>Verify</button>
      </form>
    </div>
  );
}

API (minimal, orchestration-first)

  • length: number of digits; default 6
  • mode: ‘numeric’ | ‘alphanumeric’ | RegExp
  • value / defaultValue: controlled or uncontrolled
  • onChange / onComplete: callbacks for progress and completion
  • mask: visual masking (•) only; logic remains accessible
  • separator: ‘none’ | ‘space’ | ‘dash’ | React node
  • autocomplete / inputMode: platform hints for keyboards and auto-fill

Meta-patterns

  • Headless logic hook: core OTP behaviors live in useOtp
  • Context provider: OTPProvider exposes orchestrated state
  • Slotting & substitution: UI parts are replaceable (field, label, error, separator)
  • Token-driven visuals: no hard-coded colors; styles map to tokens

Folder structure

OTPInput/
├── index.tsx
├── OTPInput.tsx            # visual scaffolding (slots)
├── OTPProvider.tsx         # context + orchestration
├── useOtp.ts               # headless logic (state, focus, paste)
├── OTPField.tsx            # default field primitive (swappable)
├── OTPSeparator.tsx        # optional slot
├── OTPLabel.tsx            # optional slot
├── OTPError.tsx            # optional slot
├── OTPInput.module.scss
├── OTPInput.tokens.json
├── OTPInput.tokens.generated.scss
└── README.md

Accessibility Notes

  • Group semantics: wrap fields in a role="group" and associate labels/errors via described-by
  • Paste: allow multi-character paste and distribute across slots
  • Backspace: backspace moves focus left when empty; clears when filled
  • Virtual keyboards: autocomplete="one-time-code", inputMode="numeric" or "tel"
  • Screen readers: each field has an aria-label (e.g., “Digit 1”)
  • Reduced motion: advance focus only on valid entry; deterministic arrow navigation

Why this travels well

  • Headless logic: products can reskin without re-implementing paste/focus/validation
  • Slotting: replace OTPField, separators, and text slots freely
  • Tokenized visuals: map typography, radius, spacing, and colors to tokens
  • Clear boundaries: OTP is a system composer; flows like checkout live as assemblies

Quick verification

  • Composer invariants: useOtp + OTPProvider exist
  • Exports include provider, group, field, and slots
  • README-style guidance covers usage, props, and a11y
  • No prop explosion; variations derive from slots + tokens

Explicit props interface

export interface OTPInputProps {
  /** Number of OTP digits (3–12 typical). Default: 6 */
  length?: number;
  /** Numeric-only, alphanumeric, or custom regex guard. Default: "numeric" */
  mode?: 'numeric' | 'alphanumeric' | RegExp;
  /** Autofill hints for platforms that support it. Default: 'one-time-code' */
  autocomplete?: 'one-time-code' | 'otp' | string;
  /** Controlled value as a string of length N (optional) */
  value?: string;
  /** Uncontrolled default value (optional) */
  defaultValue?: string;
  /** Called when all N slots are filled with valid characters */
  onComplete?(code: string): void;
  /** Called on any change (partial codes included) */
  onChange?(code: string): void;
  /** Disabled / readOnly semantics */
  disabled?: boolean;
  readOnly?: boolean;
  /** Ids for a11y grouping & descriptions (label, error, help) */
  id?: string;
  'aria-describedby'?: string;
  /** Optional mask (e.g., show • instead of digits) */
  mask?: boolean;
  /** Optional separator render strategy ('space' by default) */
  separator?: 'none' | 'space' | 'dash' | React.ReactNode;
  /** Inputmode hint to virtual keyboards; defaults inferred from mode */
  inputMode?: React.InputHTMLAttributes<HTMLInputElement>['inputMode'];
}

Tokens and styles

Example token JSON and SCSS usage:

// OTPInput.tokens.json
{
  "component": {
    "otpInput": {
      "field": {
        "size": { "minWidth": "{size.12}", "height": "{size.12}" },
        "typo": { "fontSize": "{font.size.300}", "fontWeight": "{font.weight.semibold}" },
        "radius": "{radius.md}",
        "gap": "{space.200}"
      },
      "color": {
        "text": "{color.foreground.default}",
        "bg": "{color.background.surface}",
        "border": "{color.border.subtle}",
        "focus": "{color.border.focus}",
        "invalid": "{color.border.danger}"
      }
    }
  }
}

// OTPInput.module.scss
@import './OTPInput.tokens.generated.scss';

.root {
  display: inline-flex;
  align-items: center;
  gap: var(--component-otp-field-gap);
}

.field {
  min-width: var(--component-otp-field-min-width);
  height: var(--component-otp-field-height);
  text-align: center;
  font-size: var(--component-otp-field-font-size);
  font-weight: var(--component-otp-field-font-weight);
  border-radius: var(--component-otp-field-radius);
  color: var(--component-otp-color-text);
  background: var(--component-otp-color-bg);
  border: 1px solid var(--component-otp-color-border);
  outline: none;
}

.field:focus-visible {
  border-color: var(--component-otp-color-focus);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--component-otp-color-focus) 30%, transparent);
}

Usage (slots + defaults)

import {
  OTPProvider,
  OTPInput,
  OTPField,
  OTPSeparator,
  OTPLabel,
  OTPError,
} from '@/ui/components/OTPInput';

export function CheckoutOtpExample() {
  return (
    <form onSubmit={(e) => { e.preventDefault(); }}>
      <OTPProvider length={6} mode="numeric" onComplete={(code) => console.log('OTP:', code)}>
        <OTPLabel>Enter the 6-digit code</OTPLabel>
        <OTPInput>
          {Array.from({ length: 6 }).map((_, i) => <OTPField key={i} index={i} />)}
        </OTPInput>
        <OTPError id="otp-error">{/* show error when server rejects */}</OTPError>
      </OTPProvider>
      <button type="submit">Verify</button>
    </form>
  );
}

README starter

# OTPInput

A composer for one-time passcodes (OTP). Headless logic + slot-based UI for multi-brand reuse.

## When to use
- Login, 2FA, device verification, high-risk actions.

## Key ideas
- Headless logic in useOtp (paste, focus, completion)
- Slotting: replace OTPField, OTPSeparator, OTPLabel, OTPError freely
- Tokenized visuals for brand theming

## Props
See OTPInputProps. Minimal surface: length, mode, onComplete, onChange, a11y ids, mask, separator.

## Accessibility
- Grouped with role="group", labeled and described
- autocomplete="one-time-code", inputMode hints for keyboards
- Backspace & arrow navigation semantics included
- Paste distribution supported

Original Modal Example

  • Orchestration: open/close, overlay click, focus trap handled once.
  • Slots: Header, Body, Footer as sub-components.
  • Composition: teams can put whatever primitives inside, but accessibility and focus rules are enforced.

Why Composers are Critical

  • They channel complexity into predictable patterns rather than scattered workarounds.
  • They protect accessibility models at the multi-element level (focus, ARIA roles, keyboard models).
  • They enable flexibility without chaos: slots allow teams to insert or omit, but orchestration keeps rules consistent.
  • They free product teams from rebuilding orchestration logic (which is hard, error-prone, and often missed).

Summary

Composers are the system’s conductors: they coordinate state, focus, and interaction across multiple children.

  • Examples: Modal, Form Field, Toolbar, Pagination, Rich Text Editor
  • Work of the system: orchestration, variation by pattern, slotting, context providers
  • Pitfalls: prop explosion, leaking state, accessibility drift, overgeneralization

If primitives are the boring DNA, and compounds are the grammar rules, then composers are the syntax that makes the grammar work in practice. They're where design systems prove their worth — not just in how things look, but in how they behave.

Next Steps

Composers often contain compounds and primitives, and can be combined into assemblies for complete user flows.

← Back to Component Standards