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
- What's the target browser support matrix? (e.g., IE11, mobile Safari, Chrome 80+)
- Do we need real-time validation or static data submission?
- Should the stepper support offline mode with local storage persistence?
- What's the expected accessibility level? (e.g., WCAG 2.1 AA compliance required?)
- Do we need i18n support for multi-language step titles/content?
- Are there specific design system constraints or component library requirements?
- 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 flowStepIndicator- Renders progress tracking UINavigationControls- Handles navigation logic and button states
Dumb Components (Presentational Components):
StepItem- Individual step display in indicatorActiveStep- Renders current step content with proper contextPrevButton,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
- Mounting: Initialize state, set initial step, load any pre-existing form data
- Updating: Handle step changes, validation updates, and navigation state
- Unmounting: Cleanup any side effects or subscriptions (e.g., form data persistence)
- Step Transitions: Validate before allowing navigation, handle async validation
- 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
- Step Component Caching: Only render current step content to avoid unnecessary re-renders
- Validation Memoization: Cache validation results when appropriate
- Conditional Rendering: Only render navigation controls when needed
- Component Memoization: Use React.memo for step indicators to prevent unnecessary re-renders
Accessibility Considerations
- Keyboard Navigation: Full keyboard support for step selection and navigation
- ARIA Attributes: Proper ARIA roles and labels for screen readers
- Focus Management: Maintain focus within step content during navigation
- 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:
- React Context API - For simple, component-scoped state sharing (current step, form values)
- Zustand - For complex, cross-component state (wizard metadata, progress tracking, shared validation)
- Why not Redux Toolkit? - Overkill for wizard state; simpler to maintain and debug
- 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
- Step completion: Invalidate cache when step is marked as complete
- Form changes: Clear validation errors for current and subsequent steps when form data changes
- Navigation: Clear step-specific caches when navigating to different steps
- Submission: Invalidate all cached data after successful submission
Optimistic updates design
For wizard steps with form validation:
- Validate locally before sending to server
- Show optimistic UI updates for step completion
- Revert changes if server validation fails
- 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.
Navigation Guards and Authentication Flows
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:
srcsetwith multiple sizes for different viewport widths - Image Compression: 85% quality JPEGs, 90% WebP compression
Font Loading Strategy
- Font Display:
font-display: swapfor 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:
- Component-Level Boundaries: Each step component should have its own error boundary to prevent one step's failure from breaking the entire wizard
- Wizard-Level Boundary: A top-level error boundary around the entire stepper to catch unhandled errors in step transitions or global state issues
- 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-labeloraria-labelledbyfor screen readers - 1.3.1 Info and Relationships: Proper semantic HTML structure with
role="tablist"androle="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:
langattribute 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:
-
Tab Navigation:
- Moves between step indicators in visual order
- Moves between form fields within current step
- Moves to next/previous buttons when appropriate
-
Space/Enter Activation:
- Space or Enter activates step indicators to navigate to that step
- Space or Enter activates navigation buttons (Next, Previous, Submit)
-
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-labelledbyreferences to their step labels - Error messages are announced when validation fails
- All form fields have proper
-
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:
- Announce changes to screen readers using
aria-live="polite" - Maintain focus on relevant elements when content updates
- 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: rtlandtext-align: rightto relevant elements - Adjust layout properties like
margin-left→margin-rightfor 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.languageornavigator.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
i18nextinstance 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 stepstepper:step_completed- when step is successfully completedstepper:step_skipped- when user skips a stepstepper:step_revisited- when user returns to previous stepsstepper: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
- Stepper component module - Contains all stepper-specific logic and types, no cross-feature dependencies
- Wizard feature module - Uses Stepper internally but maintains its own business logic and route handling
- Shared hooks - Reusable state management hooks that can be imported by any component needing stepper functionality
- 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
anytypes 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