Skip to main content

Stepper Wizard Component Design System

Comprehensive low-level design for reusable stepper/wizard UI components with progressive disclosure and validation patterns

This document outlines a production-ready stepper wizard implementation with step navigation, validation, and responsive design patterns for enterprise frontend systems.

1. Problem Statement & Requirements

Clarifying questions

  1. What's the target browser support matrix? (e.g., IE11, mobile Safari, Chrome 80+)
  2. Do we need real-time validation or static data submission?
  3. Should the stepper support offline mode with local storage persistence?
  4. What's the expected accessibility level? (e.g., WCAG 2.1 AA compliance required?)
  5. Do we need i18n support for multi-language step titles/content?
  6. Are there specific design system constraints or component library requirements?
  7. How many steps typically exist in a wizard flow, and what's the expected user journey pattern?

Functional requirements

  • Display steps in a linear progression with visual indicators
  • Allow navigation between steps (forward/backward)
  • Validate each step before allowing progression
  • Show progress percentage and current step position
  • Support optional steps that can be skipped
  • Enable re-entry to any step in the wizard flow
  • Display error messages inline or as notifications when validation fails
  • Maintain form state between steps (with optional persistence)
  • Support keyboard navigation and screen reader accessibility
  • Allow users to cancel or reset the wizard

Non-functional requirements

  • Performance: All step transitions should be under 100ms with no layout shifts
  • Accessibility: Full keyboard navigation and ARIA compliance (WCAG 2.1 AA)
  • Scalability: Support up to 10 steps with minimal performance degradation
  • Responsive: Adapts to mobile, tablet, and desktop form factors
  • Maintainability: Clear component boundaries and predictable state management

Constraints and assumptions

  • Wizard will be used in a single-page application context
  • Step content is dynamically loaded (lazy-loaded or fetched via API)
  • User session state will be managed by the application's auth system
  • No third-party form validation libraries will be used (custom validation logic)
  • Component will support both horizontal and vertical stepper layouts
  • Step titles and content are provided as props or via a configuration object

Out of scope

  • Multi-user collaborative wizard editing
  • Advanced validation rules (e.g., conditional validation based on other steps)
  • Drag-and-drop step reordering
  • Integration with external authentication providers
  • Print or export functionality for wizard results
  • Multi-language content translation (i18n) within the component itself

2. Component Architecture

graph TD
    A[StepperWizard] --> B[StepIndicator]
    A --> C[StepContent]
    A --> D[NavigationControls]
    B --> E[StepItem]
    C --> F[ActiveStep]
    D --> G[PrevButton]
    D --> H[NextButton]
    D --> I[SkipButton]

Component Breakdown

Smart Components (Container Components):

  • StepperWizard - Main container managing state and step flow
  • StepIndicator - Renders progress tracking UI
  • NavigationControls - Handles navigation logic and button states

Dumb Components (Presentational Components):

  • StepItem - Individual step display in indicator
  • ActiveStep - Renders current step content with proper context
  • PrevButton, NextButton, SkipButton - UI controls with event handlers

TypeScript Props Contracts

// StepperWizard props
interface StepperWizardProps {
  steps: StepConfig[];
  initialStepIndex?: number;
  onStepChange?: (stepIndex: number) => void;
  onComplete?: () => void;
  onSkip?: (stepIndex: number) => void;
  validateStep?: (stepIndex: number, formData: any) => boolean | Promise<boolean>;
  className?: string;
}

// Step configuration type
interface StepConfig {
  id: string;
  title: string;
  component: React.ComponentType<StepContentProps>;
  validate?: (formData: any) => boolean | Promise<boolean>;
  skipable?: boolean;
  description?: string;
}

// StepContent props (passed to step components)
interface StepContentProps {
  stepIndex: number;
  isCurrentStep: boolean;
  isCompleted: boolean;
  formData: any;
  setFormData: (data: any) => void;
  onNext: () => void;
  onPrev: () => void;
  onSkip: () => void;
  isValidating?: boolean;
  validationError?: string;
}

// StepIndicator props
interface StepIndicatorProps {
  steps: StepConfig[];
  currentStepIndex: number;
  onStepSelect?: (stepIndex: number) => void;
  className?: string;
}

// NavigationControls props
interface NavigationControlsProps {
  currentStepIndex: number;
  totalSteps: number;
  isLastStep: boolean;
  isFirstStep: boolean;
  isValidating?: boolean;
  canGoNext: boolean;
  onPrev: () => void;
  onNext: () => void;
  onSkip?: () => void;
  className?: string;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> Validating : Validate Step
    Validating --> Validated : Success
    Validating --> Invalid : Failed
    Validated --> NextStep : Go to next step
    Invalid --> Idle : Fix errors
    NextStep --> Idle : Step completed
    Idle --> SkipStep : Skip step
    SkipStep --> NextStep : Skip confirmed
    Idle --> Completed : All steps done
    Completed --> [*]

Component Responsibilities

StepperWizard:

  • Manages overall wizard state (current step, form data, validation status)
  • Coordinates between steps and navigation
  • Handles completion flow and callbacks
  • Implements step validation logic

StepIndicator:

  • Renders visual progress indicator
  • Highlights current and completed steps
  • Provides clickable navigation to previous steps

NavigationControls:

  • Renders appropriate navigation buttons (prev, next, skip)
  • Manages button enable/disable states based on validation
  • Handles step transitions and validation flow

ActiveStep:

  • Renders the current step's content component
  • Provides form data context to step components
  • Handles validation and error state display

Reusability Decisions

The component is designed to be highly reusable:

  • Step components are passed as React components (not hardcoded)
  • Configurable step validation logic
  • Customizable navigation controls
  • Extensible with additional props and callbacks

Component Lifecycle Considerations

  1. Mounting: Initialize state, set initial step, load any pre-existing form data
  2. Updating: Handle step changes, validation updates, and navigation state
  3. Unmounting: Cleanup any side effects or subscriptions (e.g., form data persistence)
  4. Step Transitions: Validate before allowing navigation, handle async validation
  5. Error Handling: Gracefully handle step errors and provide user feedback

State Management Integration

The StepperWizard component will:

  • Maintain form data in local state per step
  • Track validation status for each step
  • Manage navigation flow and current step index
  • Handle completion state and callback execution

Performance Considerations

  1. Step Component Caching: Only render current step content to avoid unnecessary re-renders
  2. Validation Memoization: Cache validation results when appropriate
  3. Conditional Rendering: Only render navigation controls when needed
  4. Component Memoization: Use React.memo for step indicators to prevent unnecessary re-renders

Accessibility Considerations

  1. Keyboard Navigation: Full keyboard support for step selection and navigation
  2. ARIA Attributes: Proper ARIA roles and labels for screen readers
  3. Focus Management: Maintain focus within step content during navigation
  4. Error Indicators: Clear visual and semantic error feedback for invalid steps

3. State Management Design

Local state vs. global state decisions

For a Stepper/Wizard component, we'll use a hybrid approach:

  • Local state: Step navigation state (current step index, completed steps), UI states (active/inactive), and form field values that are specific to each step
  • Global state: Wizard configuration (steps metadata, validation rules), user progress tracking across sessions, and shared data that needs to persist beyond the wizard lifecycle

State management approach

Primary choice: React Context API + Zustand for complex state

Why this combination:

  1. React Context API - For simple, component-scoped state sharing (current step, form values)
  2. Zustand - For complex, cross-component state (wizard metadata, progress tracking, shared validation)
  3. Why not Redux Toolkit? - Overkill for wizard state; simpler to maintain and debug
  4. Why not Jotai? - More complex for this use case; better suited for granular atomic state management

State shape and normalization strategy

// Wizard state structure
interface WizardState {
  // Global wizard metadata
  id: string;
  steps: StepConfig[];
  currentStepIndex: number;
  completedSteps: Set<number>;
  isSubmitted: boolean;
  
  // Step-specific form data
  formData: Record<string, any>;
  
  // Validation state
  validationErrors: Record<number, ValidationError[]>;
  
  // User session state
  progress: {
    lastVisited: Date;
    isDirty: boolean;
  };
}

interface StepConfig {
  id: string;
  title: string;
  description?: string;
  component: React.ComponentType<any>;
  validationRules?: ValidationRule[];
  isRequired?: boolean;
  shouldValidateOnExit?: boolean;
}

interface ValidationError {
  field: string;
  message: string;
}

Server state vs. client state separation

  • Client state: Step navigation, form inputs, validation errors, user progress tracking
  • Server state: Submission results, persisted wizard progress (when available), external API responses

Cache invalidation strategy

  1. Step completion: Invalidate cache when step is marked as complete
  2. Form changes: Clear validation errors for current and subsequent steps when form data changes
  3. Navigation: Clear step-specific caches when navigating to different steps
  4. Submission: Invalidate all cached data after successful submission

Optimistic updates design

For wizard steps with form validation:

  1. Validate locally before sending to server
  2. Show optimistic UI updates for step completion
  3. Revert changes if server validation fails
  4. Use error boundaries to handle submission failures gracefully

Critical-path hook implementation

// WizardContext.ts
import { createContext, useContext, useReducer, useEffect } from 'react';
import { createStore } from 'zustand/vanilla';

// Zustand store for global wizard state
const createWizardStore = () => {
  return createStore<WizardState>((set, get) => ({
    id: '',
    steps: [],
    currentStepIndex: 0,
    completedSteps: new Set(),
    isSubmitted: false,
    formData: {},
    validationErrors: {},
    progress: {
      lastVisited: new Date(),
      isDirty: false,
    },
    
    // Actions
    setCurrentStepIndex: (index: number) => set({ currentStepIndex: index }),
    
    markStepComplete: (stepIndex: number) => {
      set((state) => {
        const newCompleted = new Set(state.completedSteps);
        newCompleted.add(stepIndex);
        return {
          completedSteps: newCompleted,
          progress: { ...state.progress, lastVisited: new Date() },
        };
      });
    },
    
    updateFormData: (stepId: string, data: any) => {
      set((state) => ({
        formData: { ...state.formData, [stepId]: data },
        progress: { ...state.progress, isDirty: true },
      }));
    },
    
    setValidationErrors: (stepIndex: number, errors: ValidationError[]) => {
      set((state) => ({
        validationErrors: {
          ...state.validationErrors,
          [stepIndex]: errors,
        },
      }));
    },
    
    submitWizard: () => set({ isSubmitted: true }),
  }));
};

// Create store instance
const wizardStore = createWizardStore();

// React Context for local state
interface WizardContextType {
  currentStepIndex: number;
  setCurrentStepIndex: (index: number) => void;
  isLastStep: boolean;
  isFirstStep: boolean;
  canGoToNext: boolean;
  canGoToPrevious: boolean;
  goToStep: (index: number) => void;
  nextStep: () => void;
  previousStep: () => void;
  markStepComplete: (stepIndex: number) => void;
  isStepComplete: (stepIndex: number) => boolean;
}

const WizardContext = createContext<WizardContextType | undefined>(undefined);

// Custom hook for wizard state
export const useWizard = () => {
  const context = useContext(WizardContext);
  if (!context) {
    throw new Error('useWizard must be used within a WizardProvider');
  }
  return context;
};

// Main wizard hook with critical path logic
export const useWizardState = (wizardId: string, steps: StepConfig[]) => {
  const [state, dispatch] = useReducer(wizardReducer, {
    currentStepIndex: 0,
    steps,
    completedSteps: new Set(),
    isSubmitted: false,
  });

  // Initialize wizard store
  useEffect(() => {
    wizardStore.setState({
      id: wizardId,
      steps,
      currentStepIndex: 0,
      completedSteps: new Set(),
      isSubmitted: false,
      formData: {},
      validationErrors: {},
      progress: {
        lastVisited: new Date(),
        isDirty: false,
      },
    });
  }, [wizardId, steps]);

  // Navigation logic
  const goToStep = (index: number) => {
    if (index >= 0 && index < steps.length) {
      dispatch({ type: 'GO_TO_STEP', payload: index });
      wizardStore.setState({ currentStepIndex: index });
    }
  };

  const nextStep = () => {
    if (state.currentStepIndex < steps.length - 1) {
      dispatch({ type: 'NEXT_STEP' });
      wizardStore.setState({ currentStepIndex: state.currentStepIndex + 1 });
    }
  };

  const previousStep = () => {
    if (state.currentStepIndex > 0) {
      dispatch({ type: 'PREVIOUS_STEP' });
      wizardStore.setState({ currentStepIndex: state.currentStepIndex - 1 });
    }
  };

  const markStepComplete = (stepIndex: number) => {
    dispatch({ type: 'MARK_STEP_COMPLETE', payload: stepIndex });
    wizardStore.getState().markStepComplete(stepIndex);
  };

  const isStepComplete = (stepIndex: number) => {
    return wizardStore.getState().completedSteps.has(stepIndex);
  };

  // Computed values
  const isLastStep = state.currentStepIndex === steps.length - 1;
  const isFirstStep = state.currentStepIndex === 0;
  const canGoToNext = !isLastStep;
  const canGoToPrevious = !isFirstStep;

  return {
    // State
    currentStepIndex: state.currentStepIndex,
    steps,
    completedSteps: wizardStore.getState().completedSteps,
    isSubmitted: wizardStore.getState().isSubmitted,
    
    // Actions
    goToStep,
    nextStep,
    previousStep,
    markStepComplete,
    isStepComplete,
    
    // Computed
    isLastStep,
    isFirstStep,
    canGoToNext,
    canGoToPrevious,
  };
};

// Reducer for local state management
const wizardReducer = (state: any, action: any) => {
  switch (action.type) {
    case 'GO_TO_STEP':
      return { ...state, currentStepIndex: action.payload };
    case 'NEXT_STEP':
      return { ...state, currentStepIndex: state.currentStepIndex + 1 };
    case 'PREVIOUS_STEP':
      return { ...state, currentStepIndex: state.currentStepIndex - 1 };
    case 'MARK_STEP_COMPLETE':
      return {
        ...state,
        completedSteps: new Set([...state.completedSteps, action.payload]),
      };
    default:
      return state;
  }
};

// Return shape for the hook
interface WizardHookReturn {
  currentStepIndex: number;
  steps: StepConfig[];
  completedSteps: Set<number>;
  isSubmitted: boolean;
  goToStep: (index: number) => void;
  nextStep: () => void;
  previousStep: () => void;
  markStepComplete: (stepIndex: number) => void;
  isStepComplete: (stepIndex: number) => boolean;
  isLastStep: boolean;
  isFirstStep: boolean;
  canGoToNext: boolean;
  canGoToPrevious: boolean;
}

4. Routing & Navigation

Route Structure and Hierarchy

The stepper wizard will be structured as a nested route system with clear separation between public and protected routes:

/wizard
  ├── /step-1
  ├── /step-2
  ├── /step-3
  └── /review

Protected routes will require authentication and validation, while the wizard itself will be accessible via a protected route path. The root /wizard route will serve as the layout component that manages the stepper navigation and state.

Code Splitting Strategy

Route-based code splitting will be implemented using React.lazy and Suspense for each step component:

// routes/WizardRoutes.tsx
import { lazy, Suspense } from 'react';
import { Route, Routes, Navigate } from 'react-router-dom';

const Step1 = lazy(() => import('../components/Step1'));
const Step2 = lazy(() => import('../components/Step2'));
const Step3 = lazy(() => import('../components/Step3'));
const Review = lazy(() => import('../components/Review'));

export const WizardRoutes = () => (
  <Suspense fallback={<div>Loading...</div>}>
    <Routes>
      <Route path="/wizard" element={<WizardLayout />}>
        <Route index element={<Navigate to="/wizard/step-1" replace />} />
        <Route path="step-1" element={<Step1 />} />
        <Route path="step-2" element={<Step2 />} />
        <Route path="step-3" element={<Step3 />} />
        <Route path="review" element={<Review />} />
      </Route>
    </Routes>
  </Suspense>
);

Each step component will be split into its own chunk to reduce initial bundle size. The layout component will remain in the main bundle as it's essential for navigation.

Authentication guards will be implemented using React Router's useNavigate hook and protected route wrappers:

// guards/AuthGuard.tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../stores/authStore';

export const AuthGuard = ({ children }: { children: React.ReactNode }) => {
  const navigate = useNavigate();
  const { isAuthenticated, isLoading } = useAuthStore();

  useEffect(() => {
    if (!isAuthenticated && !isLoading) {
      navigate('/login', { replace: true });
    }
  }, [isAuthenticated, isLoading, navigate]);

  if (isLoading) {
    return <div>Loading authentication...</div>;
  }

  return children;
};

Navigation guards will also validate step completion and prevent users from navigating to steps they haven't completed yet:

// guards/StepGuard.tsx
import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useWizardStore } from '../stores/wizardStore';

export const StepGuard = ({ children }: { children: React.ReactNode }) => {
  const navigate = useNavigate();
  const params = useParams();
  const { completedSteps, currentStep } = useWizardStore();

  useEffect(() => {
    const stepNumber = parseInt(params.stepId?.replace('step-', '') || '1');
    
    if (stepNumber > 1 && !completedSteps.includes(stepNumber - 1)) {
      navigate(`/wizard/step-${stepNumber - 1}`, { replace: true });
    }
    
    if (currentStep !== stepNumber) {
      useWizardStore.setState({ currentStep: stepNumber });
    }
  }, [params.stepId, completedSteps, currentStep, navigate]);

  return children;
};

Deep Linking Support and URL Parameter Design

URL structure will follow a clean, predictable pattern:

/wizard/step-1
/wizard/step-2
/wizard/step-3
/wizard/review

Query parameters will be used for optional configuration:

/wizard?mode=edit&step=2

URL parameters will be parsed and validated in the wizard layout component:

// components/WizardLayout.tsx
import { useEffect } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { useWizardStore } from '../stores/wizardStore';

export const WizardLayout = () => {
  const [searchParams] = useSearchParams();
  const navigate = useNavigate();
  const { setCurrentStep } = useWizardStore();

  useEffect(() => {
    const mode = searchParams.get('mode');
    const stepParam = searchParams.get('step');
    
    if (mode === 'edit' && stepParam) {
      const stepNumber = parseInt(stepParam);
      if (stepNumber >= 1 && stepNumber <= 3) {
        setCurrentStep(stepNumber);
      }
    }
  }, [searchParams, setCurrentStep]);

  return (
    <div className="wizard-container">
      <WizardStepper />
      <div className="wizard-content">
        <Outlet />
      </div>
    </div>
  );
};

Browser History Management and Scroll Restoration

Scroll restoration will be handled using React Router's built-in ScrollRestoration component:

// App.tsx
import { ScrollRestoration } from 'react-router-dom';

function App() {
  return (
    <div className="app">
      <ScrollRestoration />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/wizard/*" element={<WizardRoutes />} />
      </Routes>
    </div>
  );
}

Custom scroll management will be implemented for the wizard steps to ensure proper positioning:

// components/WizardLayout.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export const WizardLayout = () => {
  const location = useLocation();
  
  useEffect(() => {
    // Scroll to top when navigating between wizard steps
    window.scrollTo({ top: 0, behavior: 'smooth' });
    
    // Add history state for proper back button behavior
    const handlePopState = () => {
      // Handle any special logic for browser back button
      console.log('Navigated back to:', location.pathname);
    };
    
    window.addEventListener('popstate', handlePopState);
    
    return () => {
      window.removeEventListener('popstate', handlePopState);
    };
  }, [location.pathname]);

  return (
    <div className="wizard-container">
      <WizardStepper />
      <div className="wizard-content">
        <Outlet />
      </div>
    </div>
  );
};

The wizard will maintain proper browser history by ensuring each step transition creates a new history entry, allowing users to navigate back using the browser's back button without losing their progress.

5. API Integration Layer

API Client Design Pattern

We'll implement a custom fetch wrapper with axios as the underlying HTTP client for robust request handling and consistent error management. This approach provides better interceptors, request cancellation, and type safety compared to vanilla fetch.

// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

const createApiClient = (): AxiosInstance => {
  const client = axios.create({
    baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
    timeout: 10000,
    headers: {
      'Content-Type': 'application/json',
    },
  });

  // Request interceptor for adding auth tokens
  client.interceptors.request.use(
    (config) => {
      const token = localStorage.getItem('authToken');
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    },
    (error) => Promise.reject(error)
  );

  // Response interceptor for handling errors
  client.interceptors.response.use(
    (response) => response,
    (error) => {
      if (error.response?.status === 401) {
        // Handle token expiration
        localStorage.removeItem('authToken');
        window.location.href = '/login';
      }
      return Promise.reject(error);
    }
  );

  return client;
};

export const apiClient = createApiClient();

Request/Response Interceptors

The interceptors handle authentication token injection, response normalization, and error transformation to provide a consistent API surface.

Error Handling and Retry Strategy

We implement exponential backoff retry logic with max 3 attempts for transient network errors:

// api/retry.ts
export const retryWithBackoff = async (
  fn: () => Promise<any>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<any> => {
  let lastError: Error;
  
  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      
      if (i === maxRetries || !shouldRetry(error)) {
        throw lastError;
      }
      
      const delay = baseDelay * Math.pow(2, i);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
};

const shouldRetry = (error: any): boolean => {
  return (
    error?.response?.status === 500 ||
    error?.response?.status === 503 ||
    error?.code === 'ECONNABORTED' ||
    error?.message.includes('network error')
  );
};

Loading, Error, and Empty States Design

We implement a standardized state management pattern for API operations:

// types/api.ts
export type ApiState<T> = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

export type StepData = {
  stepId: string;
  formData: Record<string, any>;
  status: 'pending' | 'submitted' | 'failed';
};

export type StepSubmissionResult = {
  success: boolean;
  errors?: Record<string, string>;
  data?: any;
};

Data Fetching Strategy

For a wizard component, we use client-side rendering (CSR) with progressive data loading:

  • Step 1: Load initial configuration and step metadata
  • Step 2+: Load data on demand as user navigates
  • Final Step: Submit all collected data in one batch
// hooks/useWizardData.ts
import { useState, useEffect } from 'react';
import { apiClient } from '../api/client';

export const useWizardData = (wizardId: string) => {
  const [stepData, setStepData] = useState<Record<string, any>>({});
  const [loadingStates, setLoadingStates] = useState<Record<string, boolean>>({});
  const [errorStates, setErrorStates] = useState<Record<string, string | null>>({});

  const fetchStepData = async (stepId: string) => {
    setLoadingStates(prev => ({ ...prev, [stepId]: true }));
    setErrorStates(prev => ({ ...prev, [stepId]: null }));

    try {
      const response = await apiClient.get(`/wizards/${wizardId}/steps/${stepId}`);
      setStepData(prev => ({ ...prev, [stepId]: response.data }));
    } catch (error) {
      setErrorStates(prev => ({ 
        ...prev, 
        [stepId]: error instanceof Error ? error.message : 'Failed to load step data' 
      }));
    } finally {
      setLoadingStates(prev => ({ ...prev, [stepId]: false }));
    }
  };

  const submitStep = async (stepId: string, data: any) => {
    try {
      const response = await apiClient.post(`/wizards/${wizardId}/steps/${stepId}`, data);
      setStepData(prev => ({ ...prev, [stepId]: response.data }));
      return { success: true };
    } catch (error) {
      return { 
        success: false, 
        errors: error instanceof Error ? { general: error.message } : {} 
      };
    }
  };

  return {
    stepData,
    loadingStates,
    errorStates,
    fetchStepData,
    submitStep
  };
};

API Response Transformation Layer

We implement a DTO-to-domain model mapping layer:

// api/transformers.ts
export const transformStepResponse = (dto: StepDto): Step => ({
  id: dto.id,
  title: dto.title,
  description: dto.description,
  fields: dto.fields.map(field => ({
    key: field.key,
    type: field.type,
    label: field.label,
    required: field.required,
    validation: field.validationRules?.map(rule => ({
      type: rule.type,
      message: rule.message,
      value: rule.value
    }))
  })),
  nextStepId: dto.nextStepId,
  previousStepId: dto.previousStepId
});

export const transformWizardResponse = (dto: WizardDto): Wizard => ({
  id: dto.id,
  title: dto.title,
  steps: dto.steps.map(transformStepResponse),
  createdAt: new Date(dto.createdAt),
  updatedAt: new Date(dto.updatedAt)
});

Sequence Diagram for Primary Data Flows

sequenceDiagram
    participant User
    participant WizardComponent
    participant StateManager
    participant ApiClient
    participant Server

    User->>WizardComponent: Navigate to step 2
    WizardComponent->>StateManager: Request step data
    StateManager->>ApiClient: GET /wizards/{id}/steps/2
    ApiClient->>Server: HTTP Request
    Server-->>ApiClient: JSON Response
    ApiClient-->>StateManager: Processed data
    StateManager-->>WizardComponent: Update UI state
    WizardComponent->>User: Display step content

    User->>WizardComponent: Submit step 2
    WizardComponent->>StateManager: Submit data
    StateManager->>ApiClient: POST /wizards/{id}/steps/2
    ApiClient->>Server: HTTP Request with data
    Server-->>ApiClient: Success response
    ApiClient-->>StateManager: Update success state
    StateManager-->>WizardComponent: Notify completion

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): <2.5s for all stepper steps
  • Interaction to Next Paint (INP): <200ms for step navigation and form interactions
  • Cumulative Layout Shift (CLS): <0.1 for all stepper UI elements and content transitions

Bundle Size Budget per Route

  • Stepper Component: <15KB (minified + gzipped)
  • Step Content Components: <20KB total for all steps (minified + gzipped)
  • Form Validation & State Management: <10KB (minified + gzipped)

Critical Rendering Path Optimization

  • Critical CSS: Inline essential stepper styles (step indicators, navigation buttons)
  • Lazy Loading: Step content components loaded on-demand when user navigates to step
  • Resource Hints: Preload critical assets (step icons, form validation libraries)
  • Code Splitting: Route-level code splitting with dynamic imports for each step content

Image Optimization Strategy

  • Formats: WebP for modern browsers, JPEG fallback for legacy
  • Lazy Loading: loading="lazy" attribute on all step images
  • Responsive Images: srcset with multiple sizes for different viewport widths
  • Image Compression: 85% quality JPEGs, 90% WebP compression

Font Loading Strategy

  • Font Display: font-display: swap for stepper UI fonts
  • Preload Critical Fonts: <link rel="preload" as="font"> for step indicator and navigation fonts
  • Font Subsets: Load only Latin character subset initially, load full font when needed
  • Fallback Fonts: Use system fonts as fallback for faster rendering

Memoization Strategy

What to memoize:

// Step indicator component - memoized by step index and status
const StepIndicator = React.memo(({ stepIndex, isActive, isCompleted }: StepIndicatorProps) => {
  // Component logic here
});

// Navigation buttons - memoized by disabled state and step index
const NavigationButton = React.memo(({ 
  onClick, 
  disabled, 
  label 
}: NavigationButtonProps) => {
  // Component logic here
});

What NOT to memoize:

  • Form input components (should re-render on user input)
  • Dynamic content that changes based on form state
  • Components with complex context dependencies

Virtualization for Large Lists

For stepper with many steps:

// Use react-window for large step lists (more than 10 steps)
const VirtualizedStepList = ({ steps, activeStepIndex }: VirtualizedStepListProps) => {
  const rowRenderer = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>
      <StepIndicator 
        stepIndex={index} 
        isActive={index === activeStepIndex}
        isCompleted={index < activeStepIndex}
      />
    </div>
  );

  return (
    <List
      height={300}
      itemCount={steps.length}
      itemSize={40}
      width="100%"
    >
      {rowRenderer}
    </List>
  );
};

Prefetching and Preloading Strategy

  • Step Content: Preload next step content when user is 2 steps away from current
  • Form Validation: Preload validation libraries (zod, joi) when stepper is initialized
  • API Responses: Cache API responses for steps that require server interaction
  • Critical Data: Preload form field data when step is about to be displayed
// Prefetching strategy in stepper component
const useStepperPrefetch = (currentStepIndex: number, steps: Step[]) => {
  useEffect(() => {
    // Prefetch next step content
    if (currentStepIndex < steps.length - 1) {
      const nextStep = steps[currentStepIndex + 1];
      if (nextStep.contentLoader) {
        nextStep.contentLoader();
      }
    }
    
    // Prefetch validation schemas
    if (currentStepIndex < steps.length - 2) {
      const stepAfterNext = steps[currentStepIndex + 2];
      if (stepAfterNext.validationSchema) {
        stepAfterNext.validationSchema();
      }
    }
  }, [currentStepIndex, steps]);
};

Performance Monitoring

  • Lighthouse: Target 95+ score for all stepper-related pages
  • Real User Monitoring: Track step transition times and form submission latency
  • Bundle Analysis: Monitor bundle size growth for stepper components over time
  • Memory Usage: Track component memory consumption to prevent leaks during long wizard sessions

7. Error Handling & Resilience

Error Boundary Placement Strategy

Error boundaries should be strategically placed to provide the best user experience while maintaining system stability:

  1. Component-Level Boundaries: Each step component should have its own error boundary to prevent one step's failure from breaking the entire wizard
  2. Wizard-Level Boundary: A top-level error boundary around the entire stepper to catch unhandled errors in step transitions or global state issues
  3. API Integration Boundaries: Individual API call error boundaries within each step's data loading logic
graph TD
    A[Wizard Component] --> B[Step 1 Boundary]
    A --> C[Step 2 Boundary]
    A --> D[Step 3 Boundary]
    A --> E[Global Wizard Boundary]
    
    B --> F[Step 1 Content]
    C --> G[Step 2 Content]
    D --> H[Step 3 Content]

Global Error Handler Design

A centralized error handler that captures and processes errors across the wizard:

// ErrorHandlerContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';

interface ErrorHandlerContextType {
  handleError: (error: Error, context?: string) => void;
  errors: Array<{ id: string; message: string; timestamp: Date }>;
  clearError: (id: string) => void;
}

const ErrorHandlerContext = createContext<ErrorHandlerContextType | undefined>(undefined);

export const ErrorHandlerProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [errors, setErrors] = useState<Array<{ id: string; message: string; timestamp: Date }>>([]);

  const handleError = (error: Error, context?: string) => {
    const errorId = `error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    const errorMessage = context ? `${context}: ${error.message}` : error.message;
    
    setErrors(prev => [
      ...prev,
      { id: errorId, message: errorMessage, timestamp: new Date() }
    ]);
    
    // Log to monitoring service
    console.error(`Wizard Error [${context}]:`, error);
  };

  const clearError = (id: string) => {
    setErrors(prev => prev.filter(error => error.id !== id));
  };

  return (
    <ErrorHandlerContext.Provider value={{ handleError, errors, clearError }}>
      {children}
    </ErrorHandlerContext.Provider>
  );
};

export const useErrorHandler = () => {
  const context = useContext(ErrorHandlerContext);
  if (!context) {
    throw new Error('useErrorHandler must be used within ErrorHandlerProvider');
  }
  return context;
};

Offline / Network Failure Handling

Implement robust offline resilience with graceful degradation:

// OfflineHandler.tsx
import React, { useState, useEffect } from 'react';
import { useNetworkStatus } from '../hooks/useNetworkStatus';

interface OfflineHandlerProps {
  children: React.ReactNode;
  fallbackComponent?: React.ReactNode;
}

export const OfflineHandler: React.FC<OfflineHandlerProps> = ({ 
  children, 
  fallbackComponent 
}) => {
  const [isOnline, setIsOnline] = useState(true);
  const networkStatus = useNetworkStatus();

  useEffect(() => {
    setIsOnline(navigator.onLine);
  }, [networkStatus]);

  if (!isOnline && fallbackComponent) {
    return <div className="offline-fallback">{fallbackComponent}</div>;
  }

  return <>{children}</>;
};

Graceful Degradation Patterns

When network issues occur, provide fallback experiences:

// StepperWithFallback.tsx
import React, { useState } from 'react';
import { useErrorHandler } from './ErrorHandlerContext';

interface StepperWithFallbackProps {
  steps: Array<{ id: string; title: string; component: React.ComponentType }>;
  initialStep?: string;
}

const StepperWithFallback: React.FC<StepperWithFallbackProps> = ({ 
  steps, 
  initialStep = steps[0]?.id || '' 
}) => {
  const [currentStep, setCurrentStep] = useState(initialStep);
  const [offlineMode, setOfflineMode] = useState(false);
  const { handleError } = useErrorHandler();

  const handleStepChange = (stepId: string) => {
    try {
      // Simulate network check
      if (!navigator.onLine) {
        setOfflineMode(true);
        throw new Error('No internet connection');
      }
      
      setCurrentStep(stepId);
      setOfflineMode(false);
    } catch (error) {
      handleError(error as Error, `Step change to ${stepId}`);
    }
  };

  const renderFallback = () => (
    <div className="stepper-offline-container">
      <h3>Offline Mode</h3>
      <p>You're currently offline. Some features may be limited.</p>
      <button 
        onClick={() => window.location.reload()}
        className="retry-button"
      >
        Retry Connection
      </button>
    </div>
  );

  return (
    <OfflineHandler 
      fallbackComponent={renderFallback()}
    >
      {offlineMode ? renderFallback() : (
        <Stepper 
          steps={steps} 
          currentStep={currentStep}
          onStepChange={handleStepChange}
        />
      )}
    </OfflineHandler>
  );
};

User Feedback on Errors

Implement comprehensive error feedback mechanisms:

// ToastErrorDisplay.tsx
import React, { useEffect } from 'react';
import { useErrorHandler } from './ErrorHandlerContext';

interface ToastErrorDisplayProps {
  autoClose?: boolean;
  autoCloseDelay?: number;
}

export const ToastErrorDisplay: React.FC<ToastErrorDisplayProps> = ({ 
  autoClose = true, 
  autoCloseDelay = 5000 
}) => {
  const { errors, clearError } = useErrorHandler();
  
  useEffect(() => {
    if (autoClose && errors.length > 0) {
      const timer = setTimeout(() => {
        clearError(errors[0].id);
      }, autoCloseDelay);
      
      return () => clearTimeout(timer);
    }
  }, [errors, autoClose, autoCloseDelay]);

  if (errors.length === 0) return null;

  return (
    <div className="error-toast-container">
      {errors.map((error) => (
        <div 
          key={error.id} 
          className="error-toast"
          role="alert"
        >
          <span className="error-message">{error.message}</span>
          <button 
            onClick={() => clearError(error.id)}
            className="close-button"
            aria-label="Close error"
          >
            ×
          </button>
        </div>
      ))}
    </div>
  );
};

Error Recovery Patterns

Implement recovery mechanisms for common wizard errors:

// StepperErrorRecovery.tsx
import React, { useState } from 'react';

interface RecoveryState {
  canRecover: boolean;
  recoveryAction?: () => void;
}

export const useStepperErrorRecovery = () => {
  const [recoveryState, setRecoveryState] = useState<RecoveryState>({
    canRecover: false,
    recoveryAction: undefined
  });

  const handleRecovery = (action: () => void) => {
    setRecoveryState({
      canRecover: true,
      recoveryAction: action
    });
  };

  const executeRecovery = () => {
    if (recoveryState.recoveryAction) {
      recoveryState.recoveryAction();
      setRecoveryState({ canRecover: false, recoveryAction: undefined });
    }
  };

  return {
    recoveryState,
    handleRecovery,
    executeRecovery
  };
};

Error Type Classification

Categorize errors for different handling strategies:

// ErrorTypes.ts
export type WizardErrorType = 
  | 'NETWORK_ERROR'
  | 'VALIDATION_ERROR'
  | 'DATA_FETCH_ERROR'
  | 'STEP_TRANSITION_ERROR'
  | 'USER_CANCELLED';

export interface WizardError {
  type: WizardErrorType;
  message: string;
  stepId?: string;
  timestamp: Date;
  retryable?: boolean;
}

Error State Management

Maintain error state at both step and wizard levels:

// StepperErrorState.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface StepError {
  stepId: string;
  error: WizardError;
}

interface StepperErrorState {
  globalErrors: WizardError[];
  stepErrors: StepError[];
  lastError: WizardError | null;
}

const initialState: StepperErrorState = {
  globalErrors: [],
  stepErrors: [],
  lastError: null
};

export const stepperErrorSlice = createSlice({
  name: 'stepperErrors',
  initialState,
  reducers: {
    addGlobalError: (state, action: PayloadAction<WizardError>) => {
      state.globalErrors.push(action.payload);
      state.lastError = action.payload;
    },
    addStepError: (state, action: PayloadAction<StepError>) => {
      state.stepErrors.push(action.payload);
      state.lastError = action.payload.error;
    },
    clearStepError: (state, action: PayloadAction<string>) => {
      state.stepErrors = state.stepErrors.filter(
        error => error.stepId !== action.payload
      );
    },
    clearAllErrors: (state) => {
      state.globalErrors = [];
      state.stepErrors = [];
      state.lastError = null;
    }
  }
});

export const { 
  addGlobalError, 
  addStepError, 
  clearStepError,
  clearAllErrors 
} = stepperErrorSlice.actions;

export default stepperErrorSlice.reducer;

Error Recovery Strategy

Implement specific recovery strategies for different error types:

// RecoveryStrategies.ts
import { WizardErrorType } from './ErrorTypes';

export const getRecoveryStrategy = (errorType: WizardErrorType) => {
  switch (errorType) {
    case 'NETWORK_ERROR':
      return {
        canRecover: true,
        description: 'Retry the operation or check your internet connection',
        action: () => window.location.reload()
      };
    
    case 'VALIDATION_ERROR':
      return {
        canRecover: true,
        description: 'Fix the validation errors and try again',
        action: () => null // User must fix input
      };
    
    case 'DATA_FETCH_ERROR':
      return {
        canRecover: true,
        description: 'Retry fetching the data',
        action: () => null // Retry logic handled by component
      };
    
    default:
      return {
        canRecover: false,
        description: 'This error requires manual intervention',
        action: () => null
      };
  }
};

Comprehensive Error Handling Implementation

// StepperWithErrorHandling.tsx
import React, { useState, useCallback } from 'react';
import { useErrorHandler } from './ErrorHandlerContext';
import { useStepperErrorRecovery } from './StepperErrorRecovery';

interface StepperWithErrorHandlingProps {
  steps: Array<{
    id: string;
    title: string;
    component: React.ComponentType;
  }>;
  onStepSubmit?: (stepId: string, data: any) => Promise<void>;
}

const StepperWithErrorHandling: React.FC<StepperWithErrorHandlingProps> = ({
  steps,
  onStepSubmit
}) => {
  const [currentStep, setCurrentStep] = useState(steps[0]?.id || '');
  const [stepData, setStepData] = useState<Record<string, any>>({});
  const { handleError } = useErrorHandler();
  const { recoveryState, handleRecovery, executeRecovery } = useStepperErrorRecovery();
  
  const handleStepSubmit = useCallback(async (stepId: string, data: any) => {
    try {
      if (onStepSubmit) {
        await onStepSubmit(stepId, data);
      }
      
      setStepData(prev => ({ ...prev, [stepId]: data }));
      
      // Move to next step
      const currentIndex = steps.findIndex(step => step.id === stepId);
      if (currentIndex < steps.length - 1) {
        setCurrentStep(steps[currentIndex + 1].id);
      }
    } catch (error) {
      handleError(error as Error, `Step submission failed: ${stepId}`);
      
      // Determine recovery strategy
      const errorType = (error as any).type || 'UNKNOWN_ERROR';
      handleRecovery(() => {
        // Implement recovery logic based on error type
        if (errorType === 'NETWORK_ERROR') {
          window.location.reload();
        }
      });
    }
  }, [onStepSubmit, steps, handleError, handleRecovery]);

  const retryCurrentOperation = () => {
    if (recoveryState.canRecover) {
      executeRecovery();
    }
  };

  return (
    <div className="stepper-with-error-handling">
      {recoveryState.canRecover && (
        <div className="recovery-banner">
          <p>Error occurred. {recoveryState.recoveryAction?.description}</p>
          <button onClick={retryCurrentOperation}>Retry</button>
        </div>
      )}
      
      <Stepper 
        steps={steps}
        currentStep={currentStep}
        onStepSubmit={handleStepSubmit}
      />
    </div>
  );
};

8. Testing Strategy

Unit Testing Approach

Component-level testing (70% of test coverage):

  • Test all stepper states: idle, active, completed, error
  • Validate step navigation with keyboard and mouse interactions
  • Test custom step content rendering with different component types
  • Verify error state propagation from child components
  • Test progress bar calculation and percentage updates
  • Validate step validation logic with various input states

Utility function testing (20% of test coverage):

  • Test step index calculation logic
  • Validate progress percentage computation
  • Test navigation helper functions (next, previous, jump)
  • Verify step validation utility functions

Hook testing (10% of test coverage):

  • Test custom stepper hook state transitions
  • Validate error handling and reset functionality
  • Test step completion tracking and validation

Integration Testing Strategy

Multi-step form integration:

  • End-to-end flow testing from first to last step
  • Test form data persistence across steps
  • Validate step validation cascading behavior
  • Test navigation between steps with different validation states

State synchronization:

  • Test stepper state consistency across multiple components
  • Validate progress tracking with external form state managers
  • Test step completion status synchronization

E2E Testing Scope

Core user flows:

  • Complete wizard flow from start to finish
  • Step navigation with validation errors
  • Error recovery and step revalidation
  • Progress tracking persistence across page refreshes

Testing tools:

  • Playwright for cross-browser E2E testing (Chrome, Firefox, Safari)
  • Test against mobile and desktop viewports
  • Validate keyboard-only navigation flows

Visual Regression Testing

Test coverage:

  • All stepper states (idle, active, completed, error)
  • Different step content types (text, form, image, video)
  • RTL and LTR language variations
  • Responsive breakpoints (mobile, tablet, desktop)

Accessibility Testing Automation

Axe-core integration:

  • Test WCAG 2.1 AA compliance for all stepper states
  • Validate proper ARIA attributes (role="tablist", aria-selected)
  • Check keyboard navigation accessibility
  • Verify screen reader support for step indicators

Test Data Strategy

Test fixtures:

  • Minimal valid stepper configuration (3 steps)
  • Complex multi-step wizard with validation
  • Error state scenarios with different validation types
  • Empty and loading states for async steps

Data generation:

  • Use faker.js for realistic test data in form steps
  • Generate multiple locale test cases for i18n testing
  • Create edge case scenarios (empty steps, invalid configs)

Test coverage targets:

  • 90% component unit test coverage
  • 85% utility function test coverage
  • 75% integration test coverage
  • 95% accessibility test coverage (axe-core)
  • 100% visual regression test coverage for all states

9. Accessibility (A11y)

WCAG 2.1 AA Compliance Checklist

The Stepper/Wizard component must meet all WCAG 2.1 AA success criteria:

  • 1.1.1 Non-text Content: All visual elements have appropriate aria-label or aria-labelledby for screen readers
  • 1.3.1 Info and Relationships: Proper semantic HTML structure with role="tablist" and role="tabpanel"
  • 1.3.2 Meaningful Sequence: Content flows logically in reading order
  • 1.4.1 Use of Color: Sufficient color contrast (4.5:1) for all interactive states
  • 1.4.3 Contrast (Minimum): Step indicators and text maintain minimum contrast ratios
  • 1.4.12 Text Spacing: Respects user text spacing preferences (no fixed positioning)
  • 2.1.1 Keyboard Navigation: Full keyboard accessibility for all interactive elements
  • 2.4.1 Bypass Blocks: Skip links to main content (via aria-label="skip to content")
  • 2.4.3 Focus Order: Logical tab order that matches visual reading flow
  • 2.4.6 Headings and Labels: All steps have descriptive headings
  • 3.1.1 Language: lang attribute set correctly on root element when multilingual
  • 4.1.2 Name, Role, Value: All ARIA attributes are valid and properly structured

Keyboard Navigation Support

The stepper must support full keyboard navigation:

  1. Tab Navigation:

    • Moves between step indicators in visual order
    • Moves between form fields within current step
    • Moves to next/previous buttons when appropriate
  2. Space/Enter Activation:

    • Space or Enter activates step indicators to navigate to that step
    • Space or Enter activates navigation buttons (Next, Previous, Submit)
  3. Arrow Key Navigation:

    • Left/Right arrows navigate between steps in the stepper
    • Up/Down arrows navigate between form fields within a step

Focus Management Strategy

Focus Trapping:

  • When entering a new step, focus is programmatically set to the first interactive element in that step
  • Focus is trapped within current step until user completes or navigates away

Focus Restoration:

  • When returning to a previously visited step, focus returns to the first interactive element in that step
  • Focus is restored to the step indicator when navigating between steps via keyboard

ARIA Roles and Attributes Plan

// StepIndicator component ARIA attributes
interface StepIndicatorProps {
  stepIndex: number;
  isActive: boolean;
  isCompleted: boolean;
  isDisabled?: boolean;
  onClick: () => void;
  onKeyDown: (e: React.KeyboardEvent) => void;
}

// ARIA attributes for step indicator
const StepIndicator = ({
  stepIndex,
  isActive,
  isCompleted,
  isDisabled,
  onClick,
  onKeyDown
}: StepIndicatorProps) => {
  return (
    <button
      role="tab"
      aria-selected={isActive}
      aria-expanded={isActive}
      aria-disabled={isDisabled}
      tabIndex={isActive ? 0 : -1}
      onClick={onClick}
      onKeyDown={onKeyDown}
      className={`step-indicator ${isActive ? 'active' : ''} ${isCompleted ? 'completed' : ''}`}
      aria-label={`Step ${stepIndex + 1} ${isActive ? 'current' : isCompleted ? 'completed' : ''}`}
    >
      {isCompleted ? (
        <span className="checkmark" aria-hidden="true">✓</span>
      ) : (
        <span className="step-number">{stepIndex + 1}</span>
      )}
    </button>
  );
};

Screen Reader Compatibility

  • Step Status Announcements:

    • When a step becomes active, screen readers announce "Step [number] [status]"
    • Completion status announced when step is marked as complete
  • Form Field Labels:

    • All form fields have proper aria-labelledby references to their step labels
    • Error messages are announced when validation fails
  • Progress Indication:

    • Screen readers announce current step progress (e.g., "Step 2 of 5")
    • Visual progress bar is described with aria-valuenow, aria-valuemin, aria-valuemax

Keyboard Navigation Implementation

// Keyboard navigation handler for stepper
const useStepperKeyboardNavigation = (steps: Step[], currentStepIndex: number) => {
  const handleKeyDown = useCallback((e: KeyboardEvent) => {
    if (e.key === 'ArrowLeft') {
      e.preventDefault();
      // Navigate to previous step
      if (currentStepIndex > 0) {
        navigateToStep(currentStepIndex - 1);
      }
    } else if (e.key === 'ArrowRight') {
      e.preventDefault();
      // Navigate to next step
      if (currentStepIndex < steps.length - 1) {
        navigateToStep(currentStepIndex + 1);
      }
    } else if (e.key === 'Enter' || e.key === ' ') {
      // Activate current step indicator
      if (document.activeElement?.matches('[role="tab"]')) {
        e.preventDefault();
        const stepIndex = parseInt(document.activeElement.getAttribute('aria-controls') || '');
        navigateToStep(stepIndex);
      }
    }
  }, [currentStepIndex, steps.length]);

  useEffect(() => {
    document.addEventListener('keydown', handleKeyDown);
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, [handleKeyDown]);
};

Focus Management Implementation

// Focus management for step content
const useStepFocusManagement = (stepIndex: number, isActive: boolean) => {
  const stepContentRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (isActive && stepContentRef.current) {
      // Focus first interactive element in the step
      const firstFocusable = stepContentRef.current.querySelector(
        'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
      ) as HTMLElement | null;
      
      if (firstFocusable) {
        firstFocusable.focus();
      } else {
        // If no focusable elements, focus the step container itself
        stepContentRef.current.focus();
      }
    }
  }, [isActive, stepIndex]);

  return stepContentRef;
};

Dynamic Content Updates

When steps are dynamically updated (e.g., validation errors), the component must:

  1. Announce changes to screen readers using aria-live="polite"
  2. Maintain focus on relevant elements when content updates
  3. Provide updated status information for current step
// Dynamic status announcement for screen readers
const StatusAnnouncer = ({ currentStep, stepStatus }: { 
  currentStep: number; 
  stepStatus: StepStatus[] 
}) => {
  const statusMessage = `Step ${currentStep + 1} of ${stepStatus.length} - ${
    stepStatus[currentStep]?.status || 'Not started'
  }`;

  return (
    <div 
      aria-live="polite" 
      aria-atomic="true"
      className="sr-only"
    >
      {statusMessage}
    </div>
  );
};

Testing Strategy for Accessibility

  • Automated Testing:

    • Use axe-core or pa11y to scan for WCAG violations
    • Test with screen readers (NVDA, JAWS, VoiceOver)
    • Keyboard-only navigation testing
  • Manual Testing:

    • Verify all interactive elements are keyboard accessible
    • Confirm proper focus order and visual focus indicators
    • Test with screen readers in different browsers
  • User Testing:

    • Conduct accessibility testing with users who rely on assistive technologies
    • Validate that all step navigation is intuitive for keyboard users

10. Security Considerations

XSS Prevention Strategy

The Stepper component must prevent cross-site scripting attacks through proper input sanitization and output encoding:

// Secure input handling in stepper form fields
const sanitizeInput = (value: string): string => {
  // Remove dangerous HTML tags and attributes
  return value
    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
    .replace(/on\w+\s*=\s*["'][^"']*["']/gi, '')
    .replace(/javascript:/gi, '');
};

// Secure rendering of dynamic content
const renderStepContent = (content: string) => {
  // Use DOMPurify or similar for complex content sanitization
  return DOMPurify.sanitize(content);
};

CSRF Protection

When submitting stepper data to backend endpoints, implement CSRF token validation:

// Stepper form submission with CSRF protection
const handleSubmitStep = async (stepData: StepFormData) => {
  const csrfToken = getCSRFToken(); // Retrieved from meta tags or cookies
  
  try {
    const response = await fetch('/api/submit-step', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken,
      },
      body: JSON.stringify({
        ...stepData,
        timestamp: Date.now(),
      }),
    });
    
    if (!response.ok) {
      throw new Error('CSRF validation failed');
    }
    
    return await response.json();
  } catch (error) {
    handleSecurityError(error);
  }
};

Content Security Policy (CSP) Headers

The application must enforce strict CSP headers to prevent inline script execution:

Content-Security-Policy: 
  default-src 'self'; 
  script-src 'self' 'unsafe-inline' 'unsafe-eval'; 
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;

Sensitive Data Handling in Frontend State

Sensitive data should never be stored in frontend state or local storage:

// Secure stepper state management - no sensitive data persistence
interface SafeStepperState {
  currentStep: number;
  completedSteps: Set<number>;
  // ❌ Never store sensitive data like passwords, tokens, PII here
  // ✅ Only store non-sensitive metadata
}

// Example of secure sensitive data handling
const handleSensitiveData = (step: Step) => {
  // Clear sensitive fields from state after submission
  const cleanState = {
    ...step,
    sensitiveFields: undefined, // Remove sensitive data
  };
  
  return cleanState;
};

Input Sanitization Approach

All user inputs within stepper forms must be sanitized to prevent injection attacks:

// Input validation and sanitization for stepper fields
const validateAndSanitizeStep = (stepData: any): StepData => {
  const sanitized = {};
  
  Object.entries(stepData).forEach(([key, value]) => {
    if (typeof value === 'string') {
      // Sanitize strings to prevent XSS
      sanitized[key] = sanitizeInput(value);
    } else if (Array.isArray(value)) {
      // Sanitize arrays of strings
      sanitized[key] = value.map(item => 
        typeof item === 'string' ? sanitizeInput(item) : item
      );
    } else {
      sanitized[key] = value;
    }
  });
  
  return sanitized as StepData;
};

Security Headers Implementation

Implement security headers in the application's middleware:

// Security headers for stepper-enabled routes
const securityHeaders = {
  'X-Content-Type-Options': 'nosniff',
  'X-Frame-Options': 'DENY',
  'X-XSS-Protection': '1; mode=block',
  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
  'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'",
};

Authentication and Authorization

Ensure that stepper steps are properly protected by authentication:

// Protected step navigation with auth check
const navigateToStep = (stepIndex: number) => {
  if (!isAuthenticated()) {
    // Redirect to login page or show auth modal
    redirectToLogin();
    return;
  }
  
  // Validate user has permission to access this step
  if (!canAccessStep(stepIndex)) {
    throw new Error('Unauthorized access to step');
  }
  
  setCurrentStep(stepIndex);
};

Data Validation at Multiple Layers

Implement validation both on frontend and backend to prevent malicious data:

// Frontend validation with security in mind
const validateStepSubmission = (data: StepData): boolean => {
  // Validate data structure and types
  const requiredFields = ['stepId', 'formData'];
  
  for (const field of requiredFields) {
    if (!(field in data)) {
      throw new Error(`Missing required field: ${field}`);
    }
  }
  
  // Validate step ID format (prevent injection)
  if (!/^\d+$/.test(data.stepId)) {
    throw new Error('Invalid step ID format');
  }
  
  return true;
};

Session Management

Implement proper session handling to prevent session hijacking:

// Session management for stepper workflows
const handleSessionTimeout = () => {
  // Clear sensitive session data when timeout occurs
  clearSensitiveStepperState();
  
  // Redirect to login or show session expired modal
  showSessionExpiredModal();
};

// Secure session token handling
const getSessionToken = (): string => {
  // Use HttpOnly cookies for session tokens, never store in localStorage
  return document.cookie
    .split('; ')
    .find(row => row.startsWith('sessionToken='))
    ?.split('=')[1] || '';
};

Secure Communication

All stepper data transmission must use HTTPS to prevent man-in-the-middle attacks:

// Secure API endpoint for stepper submission
const secureStepperSubmit = async (stepData: StepData) => {
  // Ensure all requests go through HTTPS only
  const baseUrl = window.location.protocol === 'https:' ? 
    'https://api.example.com' : 
    'http://localhost:3000';
    
  return fetch(`${baseUrl}/stepper/submit`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getAuthToken()}`,
    },
    body: JSON.stringify(stepData),
  });
};

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Translation keys will follow a hierarchical structure using dot notation for clear organization:

stepper
  .step
    .title
    .description
    .button
      .next
      .previous
      .submit
  .navigation
    .label
    .progress
  .errors
    .validation
    .required

RTL Support Considerations

RTL support will be handled through CSS classes and direction properties:

  • Use dir="rtl" on root container when locale is RTL
  • Apply CSS direction: rtl and text-align: right to relevant elements
  • Adjust layout properties like margin-left → margin-right for RTL compatibility
  • Ensure step indicators and progress bars render correctly in RTL context

Date, Number, Currency Formatting Strategy

For localized formatting, we'll use the built-in Intl API:

  • Dates: new Intl.DateTimeFormat(locale, { dateStyle: 'medium' })
  • Numbers: new Intl.NumberFormat(locale).format(value)
  • Currency: new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' }).format(value)

Locale Detection and Switching Strategy

Automatic Detection:

  • Detect locale from browser navigator.language or navigator.languages
  • Fall back to default locale (en-US) if no supported language found

Manual Switching:

  • Provide a locale selector in the UI (e.g., language dropdown)
  • Store selected locale in localStorage for persistence across sessions
  • Update i18next instance with new language and reload translations
// i18n configuration example
const i18nConfig = {
  supportedLngs: ['en', 'es', 'fr', 'ar'],
  fallbackLng: 'en',
  interpolation: {
    escapeValue: false,
  },
  react: {
    transSupportBasicHtmlText: true,
  },
};

Translation Key Examples

{
  "stepper": {
    "step": {
      "title": "Step {{stepNumber}}",
      "description": "Complete this step to continue",
      "button": {
        "next": "Next Step",
        "previous": "Previous Step",
        "submit": "Submit"
      }
    },
    "navigation": {
      "label": "Progress: {{currentStep}} of {{totalSteps}}",
      "progress": "Completed {{completedSteps}} of {{totalSteps}}"
    },
    "errors": {
      "validation": "Please fix validation errors",
      "required": "This field is required"
    }
  }
}

Implementation Details

The component will integrate with i18next or react-i18next for translation management. All user-facing strings will be wrapped with translation functions:

const t = useTranslation().t;

// Usage in component
<h2>{t('stepper.step.title', { stepNumber: currentStep })}</h2>

RTL handling will be implemented through CSS modules or styled-components with conditional class application:

const Stepper = ({ isRTL }: { isRTL: boolean }) => (
  <div className={isRTL ? 'rtl' : 'ltr'}>
    {/* stepper content */}
  </div>
);

The i18n implementation will support dynamic language switching without full page reload, maintaining stepper state during locale changes.

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The Stepper component should track key performance indicators to understand user behavior and system health:

  • Step Completion Time: Time taken to complete each step (from mount to submit/click next)
  • Step Navigation Latency: Time between user action and step transition
  • Error Rate Per Step: Percentage of failed transitions per step
  • Abandonment Rate: Percentage of users who leave without completing the wizard
  • Step Revisit Count: How often users return to previous steps
// Example RUM tracking hook
const useStepperRUM = () => {
  const [stepTimings, setStepTimings] = useState<Record<string, number>>({});
  
  const trackStepCompletion = (stepId: string, duration: number) => {
    // Send to RUM service
    const payload = {
      metric: 'step_completion_time',
      stepId,
      duration,
      timestamp: Date.now(),
      userId: getCurrentUserId()
    };
    
    // Example: send to custom RUM endpoint or third-party service
    fetch('/api/rum', {
      method: 'POST',
      body: JSON.stringify(payload),
      headers: { 'Content-Type': 'application/json' }
    });
  };
  
  return { trackStepCompletion };
};

Error Tracking Integration

All stepper-related errors should be captured and reported to error tracking services:

  • Step Validation Failures: Client-side validation errors
  • API Request Failures: Network errors during step submission
  • State Management Errors: Invalid state transitions or corrupted wizard state
  • User Interaction Errors: Failed navigation attempts
// Error tracking integration pattern
const useStepperErrorTracking = () => {
  const reportError = (error: Error, context: string) => {
    // Send to Sentry or similar error tracking service
    if (window.Sentry) {
      window.Sentry.captureException(error, {
        tags: { component: 'Stepper', context },
        extra: { error, timestamp: Date.now() }
      });
    }
    
    // Fallback to console logging for development
    if (process.env.NODE_ENV === 'development') {
      console.error(`[Stepper Error] ${context}:`, error);
    }
  };
  
  return { reportError };
};

Custom Events and User Journey Tracking

Capture user journey events to understand how users interact with the wizard:

  • stepper:step_entered - when user enters a new step
  • stepper:step_completed - when step is successfully completed
  • stepper:step_skipped - when user skips a step
  • stepper:step_revisited - when user returns to previous steps
  • stepper:form_submitted - when final form is submitted
// User journey tracking implementation
const useStepperAnalytics = () => {
  const trackEvent = (eventName: string, properties?: Record<string, any>) => {
    const eventPayload = {
      eventName,
      timestamp: Date.now(),
      userId: getCurrentUserId(),
      stepId: properties?.stepId || null,
      ...properties
    };
    
    // Send to analytics service (e.g., Mixpanel, Amplitude)
    if (window.analytics) {
      window.analytics.track(eventName, eventPayload);
    }
  };
  
  return { trackEvent };
};

Performance Monitoring and Regression Detection

Monitor key performance thresholds to detect regressions:

  • Step Load Time: Ensure each step loads within 200ms (target)
  • Navigation Smoothness: <50ms between step transitions
  • Memory Usage: Prevent memory leaks in wizard state management
  • Bundle Size: Keep stepper component under 5KB (compressed) for optimal loading
// Performance monitoring hook
const useStepperPerformance = () => {
  const [performanceMetrics, setPerformanceMetrics] = useState({
    stepLoadTimes: [] as number[],
    navigationLatencies: [] as number[],
    memoryUsage: 0
  });
  
  const measureStepLoad = (stepId: string, loadTime: number) => {
    setPerformanceMetrics(prev => ({
      ...prev,
      stepLoadTimes: [...prev.stepLoadTimes, { stepId, loadTime }]
    }));
    
    // Alert if load time exceeds threshold
    if (loadTime > 200) {
      console.warn(`[Performance Alert] Step ${stepId} took ${loadTime}ms to load`);
    }
  };
  
  return { performanceMetrics, measureStepLoad };
};

Dashboard Metrics Integration

Dashboard-level metrics should include:

  • Wizard Completion Rate: Overall success rate of wizard flows
  • Step Drop-off Rate: Percentage of users dropping off at each step
  • Average Time to Complete: Total time taken across all steps
  • Peak Concurrent Users: Maximum users using the wizard simultaneously

These metrics can be visualized in a dashboard to monitor system health and user experience quality.

13. File & Folder Structure

Proposed Directory Structure

src/
├── components/
│   ├── Stepper/
│   │   ├── index.ts
│   │   ├── Stepper.tsx
│   │   ├── Step.tsx
│   │   ├── StepContent.tsx
│   │   ├── StepperContext.tsx
│   │   └── types.ts
│   ├── Wizard/
│   │   ├── index.ts
│   │   ├── Wizard.tsx
│   │   └── types.ts
├── features/
│   ├── onboarding/
│   │   ├── index.ts
│   │   └── OnboardingWizard.tsx
├── hooks/
│   ├── useStepper.ts
│   └── useWizard.ts
├── types/
│   ├── stepper.ts
│   └── wizard.ts
└── utils/
    └── stepper-utils.ts

Module Boundaries and Import Rules

  1. Stepper component module - Contains all stepper-specific logic and types, no cross-feature dependencies
  2. Wizard feature module - Uses Stepper internally but maintains its own business logic and route handling
  3. Shared hooks - Reusable state management hooks that can be imported by any component needing stepper functionality
  4. Type definitions - Centralized type definitions that are imported by both components and hooks

Shared vs Feature-Specific Code Separation

Shared code:

  • Core Stepper component and its context
  • Reusable utility functions for step navigation
  • Common TypeScript types and interfaces

Feature-specific code:

  • Wizard implementation that composes Stepper
  • Onboarding-specific step content and validation logic
  • Feature-level hooks that extend core stepper functionality

This structure enables:

  • Clear separation of concerns between generic stepper logic and feature-specific implementations
  • Easy reusability across different wizard flows (onboarding, checkout, setup)
  • Maintainable component hierarchy with proper encapsulation
  • Scalable architecture that supports multiple wizard types without cross-contamination

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

1. React Hook Form

  • Justification: Provides robust form validation, error handling, and field-level state management with minimal re-renders. Essential for managing complex wizard forms with multiple steps and validation rules.
  • Bundle Impact: ~15KB gzipped
  • Alternatives Considered: Formik (more verbose, less performant) and native React state management (too verbose for complex forms)

2. React Router DOM

  • Justification: Required for route-based navigation between wizard steps, handling URL parameters, and implementing proper browser history management. Enables clean separation of step content.
  • Bundle Impact: ~20KB gzipped
  • Alternatives Considered: Custom routing solution (too complex and error-prone)

3. React Query

  • Justification: Excellent for managing server state, caching, and optimistic updates when wizard steps involve API calls. Provides automatic refetching and error handling.
  • Bundle Impact: ~12KB gzipped
  • Alternatives Considered: Redux Toolkit (overkill for wizard state, more boilerplate)

4. Styled Components / Tailwind CSS

  • Justification: For consistent, maintainable styling with theme support. Tailwind provides utility-first approach that's ideal for rapid wizard UI development.
  • Bundle Impact: ~15KB (Tailwind) or ~20KB (Styled Components)
  • Alternatives Considered: CSS Modules (less maintainable) and CSS-in-JS libraries without strong type support

5. Framer Motion

  • Justification: Enables smooth, performant animations for step transitions and interactive feedback. Critical for enhancing UX in multi-step workflows.
  • Bundle Impact: ~10KB gzipped
  • Alternatives Considered: CSS transitions (limited control) and GSAP (overkill for simple animations)

6. Axios

  • Justification: Robust HTTP client with request/response interceptors, error handling, and cancellation support. Essential for API integration in wizard steps.
  • Bundle Impact: ~15KB gzipped
  • Alternatives Considered: Fetch API (more boilerplate, less built-in features)

Alternatives Considered and Rejected

1. Formik

  • Rejected: More verbose than React Hook Form, requires more boilerplate code for validation and error handling. Less performant with frequent re-renders.

2. Redux Toolkit

  • Rejected: Overkill for wizard state management. Wizard steps have well-defined lifecycle and don't require complex global state management.

3. Material-UI

  • Rejected: While it provides a stepper component, it introduces significant bundle size overhead (~50KB+). Custom implementation offers better control and smaller footprint.

4. React-Form

  • Rejected: Less mature ecosystem and documentation compared to React Hook Form, with fewer community resources.

Licensing and Bundle Size Impact Assessment

Bundle Size Budget Allocation:

  • Core Wizard Components: ~25KB (excluding dependencies)
  • State Management & Routing: ~35KB
  • UI/Animation Libraries: ~25KB
  • API Integration Layer: ~15KB

Total Estimated Bundle Size: ~100KB gzipped (under target 150KB budget for medium-sized feature)

Licensing Considerations:

  • All selected libraries are MIT licensed with permissive usage terms
  • No commercial restrictions for production use
  • Open source ecosystem with active maintenance and community support

Tree-shaking Potential:

  • React Hook Form and React Query support tree-shaking for minimal bundle inclusion
  • Framer Motion allows selective import of animation features
  • Tailwind CSS can be configured for minimal class inclusion

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Step validation strategy: Should steps be validated before navigation, or only on final submission? This impacts UX consistency and error handling complexity.
  • Progress persistence: Should step progress be persisted in localStorage or session storage? This affects data consistency and user experience across browser sessions.
  • Step reordering capability: Should steps be reorderable by users? This significantly increases complexity in state management and validation logic.
  • Conditional step rendering: Should steps be conditionally rendered based on previous step responses? This requires dynamic route configuration and state dependencies.

Known trade-offs made in this design

  • Component reusability vs. specificity: Prioritized component flexibility over strict type safety by using generic props, potentially increasing runtime errors
  • State management complexity vs. performance: Chose React Context for global state to avoid prop drilling, but this introduces potential re-render overhead
  • UI flexibility vs. accessibility: Simplified keyboard navigation for better maintainability, potentially compromising edge-case accessibility support
  • Client-side validation vs. server-side validation: Implemented client-side validation for better UX, but this creates a potential security gap if not also validated server-side

Risks and mitigations

  • State drift risk: With multiple state sources (local, global, localStorage), there's risk of inconsistent state. Mitigation: Implement strict state normalization and validation hooks.
  • Performance degradation: Large wizard with many steps could cause performance issues. Mitigation: Implement virtualization for step content and lazy loading of heavy components.
  • Accessibility regression: Complex navigation patterns could break screen reader support. Mitigation: Comprehensive ARIA implementation with automated accessibility testing.
  • Offline handling complexity: Users may lose progress in offline scenarios. Mitigation: Implement robust localStorage persistence with sync strategies.

Technical debt acknowledged

  • Type safety compromise: Using generic any types in some component props to maintain flexibility, creating potential runtime errors
  • Hardcoded step configuration: Steps are currently hardcoded in component files rather than dynamically loaded from configuration, limiting extensibility
  • Limited internationalization support: Basic i18n support is implemented but not fully tested across all locales and RTL scenarios
  • Error boundary scope: Error boundaries are implemented at component level but not granular enough to handle partial step failures without full wizard crash
  • Testing coverage gaps: Unit tests cover basic functionality but don't fully exercise edge cases like rapid navigation or concurrent step modifications

Was this article helpful?

Topics in this article

More from Frontend LLD

  • Frontend HLD

    Search and Autocomplete Frontend System Design

    Comprehensive frontend high-level design for scalable search and autocomplete functionality with real-time suggestions and performance optimization

  • Frontend LLD

    Permission-Based UI Rendering System Design

    Comprehensive low-level design for implementing role-based and attribute-based access control in frontend applications with dynamic UI component rendering.

  • Frontend LLD

    Comprehensive Toast Notification System Design

    Detailed low-level design for a scalable, accessible toast notification system with queue management and customizable behavior patterns

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.