form-ux-patterns

安装量: 40
排名: #17957

安装

npx skills add https://github.com/bbeierle12/skill-mcp-claude --skill form-ux-patterns

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 (

); }

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

{/* 12 fields = cognitive overload */}

// ✅ GOOD: Chunked into logical groups (5-7 max per group)

Contact (2 fields)
Shipping (5 fields)
Payment (4 fields)
  1. Briefing vs. Checklist (Aviation Principle)

Instructions should be separate from labels, given before the task.

// ❌ BAD: Instructions mixed with labels

// ✅ GOOD: Briefing before, label during

Create a strong password with:

  • At least 8 characters
  • Uppercase and lowercase letters
  • At least one number

  1. 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 (

  {/* 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) => 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; 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;

/* 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 (

返回排行榜