Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
bbeierle12 avatar

Form Ux Patterns

  • 102 installs
  • 8 repo stars
  • Updated August 4, 2026
  • bbeierle12/skill-mcp-claude

form-ux-patterns is a Claude Code skill that provides UX patterns for complex forms, including multi-step wizards, cognitive chunking, progressive disclosure, and conditional fields.

About

form-ux-patterns is a Claude Code skill for complex form UX, built on cognitive-load and aviation UX principles. It covers multi-step wizards, chunking fields into groups of 5-7, progressive disclosure, and conditional fields. A developer loads it when building checkout flows, onboarding wizards, or forms with many fields. It provides a useMultiStepForm hook and step configuration types.

  • Multi-step wizard hook with per-step validation and progress
  • Cognitive chunking to 5-7 fields per group (Miller's Law)
  • Progressive disclosure and conditional-field patterns for checkout and onboarding

Form Ux Patterns by the numbers

  • 102 all-time installs (skills.sh)
  • Ranked #1,052 of 2,245 Frontend Development skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
At a glance

form-ux-patterns capabilities & compatibility

Capabilities
form ux · multi step forms · react forms · form validation
Use cases
frontend
From the docs

What form-ux-patterns says it does

Patterns for complex forms based on cognitive load research and aviation UX principles.
SKILL.md
"Humans can hold 5-7 items in working memory" — Miller's Law
SKILL.md
npx skills add https://github.com/bbeierle12/skill-mcp-claude --skill form-ux-patterns

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs102
repo stars8
Last updatedAugust 4, 2026
Repositorybbeierle12/skill-mcp-claude

What it does

Build multi-step wizards and long forms with chunking, progressive disclosure, and conditional fields.

Who is it for?

Developers building checkout flows, onboarding wizards, or forms with many fields.

Skip if: Simple single-section forms where wizard overhead is unnecessary.

When should I use this skill?

Building checkout flows, onboarding wizards, or forms with many fields.

What you get

Chunked multi-step forms with per-step validation, progress, and conditional disclosure.

  • Multi-step form hook
  • Step and chunk configuration types
  • Progressive disclosure patterns

By the numbers

  • Recommends max 5-7 fields per group
  • 12-field example shown as cognitive overload to be chunked

Files

SKILL.mdMarkdownGitHub ↗

Form UX Patterns

Patterns for complex forms based on cognitive load research and aviation UX principles.

Quick Start

// Multi-step form with chunking
import { useMultiStepForm } from './multi-step-form';

function CheckoutWizard() {
  const { currentStep, steps, goNext, goBack, isLastStep } = useMultiStepForm({
    steps: [
      { id: 'contact', title: 'Contact', fields: ['email', 'phone'] },
      { id: 'shipping', title: 'Shipping', fields: ['name', 'street', 'city', 'state', 'zip'] },
      { id: 'payment', title: 'Payment', fields: ['cardName', 'cardNumber', 'expiry', 'cvv'] }
    ]
  });

  return (
    <form>
      <StepIndicator steps={steps} current={currentStep} />
      <StepContent step={steps[currentStep]} />
      <StepNavigation onBack={goBack} onNext={goNext} isLast={isLastStep} />
    </form>
  );
}

Core Principles

1. Cognitive Chunking (Aviation Principle)

"Humans can hold 5-7 items in working memory" — Miller's Law
// ❌ BAD: All fields on one page
<form>
  <input name="email" />
  <input name="phone" />
  <input name="name" />
  <input name="street" />
  <input name="street2" />
  <input name="city" />
  <input name="state" />
  <input name="zip" />
  <input name="cardName" />
  <input name="cardNumber" />
  <input name="expiry" />
  <input name="cvv" />
  {/* 12 fields = cognitive overload */}
</form>

// ✅ GOOD: Chunked into logical groups (5-7 max per group)
<form>
  <fieldset>
    <legend>Contact (2 fields)</legend>
    <input name="email" />
    <input name="phone" />
  </fieldset>
  
  <fieldset>
    <legend>Shipping (5 fields)</legend>
    <input name="name" />
    <input name="street" />
    <input name="city" />
    <input name="state" />
    <input name="zip" />
  </fieldset>
  
  <fieldset>
    <legend>Payment (4 fields)</legend>
    <input name="cardName" />
    <input name="cardNumber" />
    <input name="expiry" />
    <input name="cvv" />
  </fieldset>
</form>

2. Briefing vs. Checklist (Aviation Principle)

Instructions should be separate from labels, given before the task.
// ❌ BAD: Instructions mixed with labels
<label>
  Password (must be 8+ characters with uppercase, lowercase, and number)
</label>
<input type="password" />

// ✅ GOOD: Briefing before, label during
<div className="field-briefing">
  <p>Create a strong password with:</p>
  <ul>
    <li>At least 8 characters</li>
    <li>Uppercase and lowercase letters</li>
    <li>At least one number</li>
  </ul>
</div>

<label>Password</label>
<input type="password" />

3. Progressive Disclosure

Show only what's needed, when it's needed.
// Reveal fields based on selection
function ShippingForm() {
  const [method, setMethod] = useState<'standard' | 'express' | 'pickup'>('standard');

  return (
    <form>
      <RadioGroup
        label="Delivery method"
        value={method}
        onChange={setMethod}
        options={[
          { value: 'standard', label: 'Standard (5-7 days)' },
          { value: 'express', label: 'Express (2-3 days)' },
          { value: 'pickup', label: 'Store pickup' }
        ]}
      />

      {/* Only show address for shipping methods */}
      {method !== 'pickup' && (
        <AddressFields />
      )}

      {/* Only show store selector for pickup */}
      {method === 'pickup' && (
        <StoreSelector />
      )}
    </form>
  );
}

Multi-Step Forms

Step Configuration

// types/multi-step.ts
export interface FormStep {
  /** Unique step identifier */
  id: string;
  
  /** Display title */
  title: string;
  
  /** Optional description (briefing) */
  description?: string;
  
  /** Fields in this step (for validation) */
  fields: string[];
  
  /** Zod schema for this step */
  schema?: z.ZodType;
  
  /** Whether step can be skipped */
  optional?: boolean;
  
  /** Condition for showing this step */
  condition?: (formData: Record<string, any>) => boolean;
}

export interface FormChunk {
  /** Chunk identifier */
  id: string;
  
  /** Chunk title */
  title: string;
  
  /** Briefing text (shown before fields) */
  briefing?: string;
  
  /** Fields in this chunk (max 5-7) */
  fields: string[];
}

Multi-Step Hook

// hooks/use-multi-step-form.ts
import { useState, useCallback, useMemo } from 'react';
import { UseFormReturn } from 'react-hook-form';

export interface UseMultiStepFormOptions {
  steps: FormStep[];
  form: UseFormReturn<any>;
  onComplete?: (data: any) => void;
}

export interface UseMultiStepFormReturn {
  /** Current step index */
  currentStep: number;
  
  /** Current step config */
  step: FormStep;
  
  /** All steps (filtered by conditions) */
  steps: FormStep[];
  
  /** Total step count */
  totalSteps: number;
  
  /** Whether on first step */
  isFirstStep: boolean;
  
  /** Whether on last step */
  isLastStep: boolean;
  
  /** Progress percentage (0-100) */
  progress: number;
  
  /** Go to next step (validates current) */
  goNext: () => Promise<boolean>;
  
  /** Go to previous step */
  goBack: () => void;
  
  /** Go to specific step */
  goTo: (index: number) => void;
  
  /** Can navigate to step (all previous valid) */
  canGoTo: (index: number) => boolean;
}

export function useMultiStepForm({
  steps: allSteps,
  form,
  onComplete
}: UseMultiStepFormOptions): UseMultiStepFormReturn {
  const [currentStep, setCurrentStep] = useState(0);
  
  // Filter steps by conditions
  const steps = useMemo(() => {
    const data = form.getValues();
    return allSteps.filter(step => 
      !step.condition || step.condition(data)
    );
  }, [allSteps, form]);
  
  const step = steps[currentStep];
  const totalSteps = steps.length;
  const isFirstStep = currentStep === 0;
  const isLastStep = currentStep === totalSteps - 1;
  const progress = ((currentStep + 1) / totalSteps) * 100;
  
  const goNext = useCallback(async () => {
    // Validate current step fields
    const isValid = await form.trigger(step.fields as any);
    
    if (!isValid) {
      // Focus first error
      const firstError = document.querySelector('[aria-invalid="true"]');
      (firstError as HTMLElement)?.focus();
      return false;
    }
    
    if (isLastStep) {
      // Submit form
      const data = form.getValues();
      onComplete?.(data);
    } else {
      setCurrentStep(prev => prev + 1);
      // Focus step heading
      requestAnimationFrame(() => {
        document.getElementById('step-heading')?.focus();
      });
    }
    
    return true;
  }, [step, isLastStep, form, onComplete]);
  
  const goBack = useCallback(() => {
    if (!isFirstStep) {
      setCurrentStep(prev => prev - 1);
      requestAnimationFrame(() => {
        document.getElementById('step-heading')?.focus();
      });
    }
  }, [isFirstStep]);
  
  const goTo = useCallback((index: number) => {
    if (index >= 0 && index < totalSteps) {
      setCurrentStep(index);
    }
  }, [totalSteps]);
  
  const canGoTo = useCallback((index: number) => {
    // Can always go back
    if (index < currentStep) return true;
    
    // Can only go forward if all previous steps are valid
    // (would need form state tracking for this)
    return index <= currentStep;
  }, [currentStep]);
  
  return {
    currentStep,
    step,
    steps,
    totalSteps,
    isFirstStep,
    isLastStep,
    progress,
    goNext,
    goBack,
    goTo,
    canGoTo
  };
}

Step Indicator Component

// components/StepIndicator.tsx
interface StepIndicatorProps {
  steps: FormStep[];
  currentStep: number;
  onStepClick?: (index: number) => void;
  canNavigate?: (index: number) => boolean;
}

export function StepIndicator({
  steps,
  currentStep,
  onStepClick,
  canNavigate
}: StepIndicatorProps) {
  return (
    <nav aria-label="Form progress">
      <ol className="step-indicator">
        {steps.map((step, index) => {
          const status = index < currentStep 
            ? 'complete' 
            : index === currentStep 
              ? 'current' 
              : 'upcoming';
          
          const clickable = canNavigate?.(index) ?? false;
          
          return (
            <li 
              key={step.id}
              className={`step-indicator__item step-indicator__item--${status}`}
            >
              {clickable ? (
                <button
                  type="button"
                  onClick={() => onStepClick?.(index)}
                  aria-current={status === 'current' ? 'step' : undefined}
                >
                  <span className="step-indicator__number">{index + 1}</span>
                  <span className="step-indicator__title">{step.title}</span>
                </button>
              ) : (
                <span aria-current={status === 'current' ? 'step' : undefined}>
                  <span className="step-indicator__number">{index + 1}</span>
                  <span className="step-indicator__title">{step.title}</span>
                </span>
              )}
            </li>
          );
        })}
      </ol>
      
      {/* Progress bar */}
      <div 
        className="step-indicator__progress"
        role="progressbar"
        aria-valuenow={currentStep + 1}
        aria-valuemin={1}
        aria-valuemax={steps.length}
        aria-label={`Step ${currentStep + 1} of ${steps.length}`}
      >
        <div 
          className="step-indicator__progress-fill"
          style={{ width: `${((currentStep + 1) / steps.length) * 100}%` }}
        />
      </div>
    </nav>
  );
}

Step Navigation Component

// components/StepNavigation.tsx
interface StepNavigationProps {
  onBack: () => void;
  onNext: () => void;
  isFirstStep: boolean;
  isLastStep: boolean;
  isSubmitting?: boolean;
  backLabel?: string;
  nextLabel?: string;
  submitLabel?: string;
}

export function StepNavigation({
  onBack,
  onNext,
  isFirstStep,
  isLastStep,
  isSubmitting = false,
  backLabel = 'Back',
  nextLabel = 'Continue',
  submitLabel = 'Submit'
}: StepNavigationProps) {
  return (
    <div className="step-navigation">
      {!isFirstStep && (
        <button
          type="button"
          onClick={onBack}
          className="step-navigation__back"
          disabled={isSubmitting}
        >
          {backLabel}
        </button>
      )}
      
      <button
        type="button"
        onClick={onNext}
        className="step-navigation__next"
        disabled={isSubmitting}
      >
        {isSubmitting ? (
          <>
            <Spinner aria-hidden="true" />
            <span className="sr-only">Processing...</span>
            Processing...
          </>
        ) : (
          isLastStep ? submitLabel : nextLabel
        )}
      </button>
    </div>
  );
}

Conditional Fields

Pattern: Show/Hide Based on Selection

// components/ConditionalField.tsx
import { useFormContext, useWatch } from 'react-hook-form';
import { ReactNode } from 'react';

interface ConditionalFieldProps {
  /** Field to watch */
  watch: string;
  
  /** Condition for showing children */
  when: (value: any) => boolean;
  
  /** Children to render when condition is true */
  children: ReactNode;
  
  /** Whether to keep values when hidden */
  keepValues?: boolean;
}

export function ConditionalField({
  watch: watchField,
  when,
  children,
  keepValues = false
}: ConditionalFieldProps) {
  const { control, unregister } = useFormContext();
  const value = useWatch({ control, name: watchField });
  
  const shouldShow = when(value);
  
  // Optionally unregister fields when hidden
  useEffect(() => {
    if (!shouldShow && !keepValues) {
      // Get field names from children and unregister
      // (implementation depends on your field structure)
    }
  }, [shouldShow, keepValues]);
  
  if (!shouldShow) return null;
  
  return <>{children}</>;
}

// Usage
<FormField name="hasCompany" label="Are you a business?" type="checkbox" />

<ConditionalField watch="hasCompany" when={(v) => v === true}>
  <FormField name="companyName" label="Company name" />
  <FormField name="taxId" label="Tax ID" />
</ConditionalField>

Pattern: Dynamic Field Array

// components/RepeatableField.tsx
import { useFieldArray, useFormContext } from 'react-hook-form';

interface RepeatableFieldProps {
  name: string;
  label: string;
  maxItems?: number;
  minItems?: number;
  renderItem: (index: number) => ReactNode;
}

export function RepeatableField({
  name,
  label,
  maxItems = 10,
  minItems = 1,
  renderItem
}: RepeatableFieldProps) {
  const { control } = useFormContext();
  const { fields, append, remove } = useFieldArray({ control, name });
  
  const canAdd = fields.length < maxItems;
  const canRemove = fields.length > minItems;
  
  return (
    <fieldset className="repeatable-field">
      <legend>{label}</legend>
      
      {fields.map((field, index) => (
        <div key={field.id} className="repeatable-field__item">
          {renderItem(index)}
          
          {canRemove && (
            <button
              type="button"
              onClick={() => remove(index)}
              aria-label={`Remove item ${index + 1}`}
            >
              Remove
            </button>
          )}
        </div>
      ))}
      
      {canAdd && (
        <button
          type="button"
          onClick={() => append({})}
          className="repeatable-field__add"
        >
          Add {label.toLowerCase()}
        </button>
      )}
    </fieldset>
  );
}

// Usage
<RepeatableField
  name="teammates"
  label="Team Members"
  maxItems={5}
  renderItem={(index) => (
    <>
      <FormField name={`teammates.${index}.name`} label="Name" />
      <FormField name={`teammates.${index}.email`} label="Email" />
    </>
  )}
/>

Form Layout Patterns

Single Column (Recommended Default)

// Best for most forms - clear visual flow
<form className="form-layout--single">
  <FormField name="email" label="Email" />
  <FormField name="password" label="Password" />
  <button type="submit">Sign in</button>
</form>

// CSS
.form-layout--single {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-width: 400px;
}

Two Column (Use Sparingly)

// Only for related short fields
<form className="form-layout--two-col">
  <FormField name="firstName" label="First name" />
  <FormField name="lastName" label="Last name" />
  
  <FormField name="city" label="City" className="col-span-1" />
  <FormField name="state" label="State" className="col-span-1" />
</form>

// CSS
.form-layout--two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}

@media (max-width: 640px) {
  .form-layout--two-col {
    grid-template-columns: 1fr;
  }
}

Card Sections

// For long forms with distinct sections
<form className="form-layout--cards">
  <section className="form-card">
    <h3>Contact Information</h3>
    <FormField name="email" label="Email" />
    <FormField name="phone" label="Phone" />
  </section>
  
  <section className="form-card">
    <h3>Shipping Address</h3>
    <AddressFields />
  </section>
  
  <section className="form-card">
    <h3>Payment</h3>
    <PaymentFields />
  </section>
</form>

File Structure

form-ux-patterns/
├── SKILL.md
├── references/
│   ├── cognitive-load.md       # Research on chunking
│   └── wizard-patterns.md      # Multi-step best practices
└── scripts/
    ├── multi-step-form.tsx     # Multi-step hook + components
    ├── conditional-field.tsx   # Show/hide patterns
    ├── repeatable-field.tsx    # Dynamic arrays
    ├── step-indicator.tsx      # Progress indicator
    └── step-indicator.css      # Styles

Reference

  • references/cognitive-load.md — Research on Miller's Law and chunking
  • references/wizard-patterns.md — Multi-step wizard best practices

Related skills

FAQ

How many fields per group does it recommend?

A maximum of 5-7 fields per logical group, based on Miller's Law about working memory.

What is it best suited for?

Checkout flows, onboarding wizards, and forms with many fields.

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.