Steppers
Steppers control navigation in multi-step forms, providing progress indication and navigation controls.
Available Steppers
Import from @schema-engine/forms-react/registrations:
import {
DefaultStepperRegistration,
MinimalistStepperRegistration,
DebuggerStepperRegistration,
} from '@schema-engine/forms-react/registrations';| Stepper | Description |
|---|---|
default | Horizontal progress indicator with step numbers |
minimalist | Clean, minimal progress bar |
debugger | Development debugging tool showing form state |
How Steppers Work
A stepper is configured at the form level and applies to all steps:
const formConfig: FormConfig = {
id: 'registration',
steps: [
{ id: 'personal', title: 'Personal Information', elements: [...] },
{ id: 'account', title: 'Account Details', elements: [...] },
{ id: 'preferences', title: 'Preferences', elements: [...] },
],
stepper: {
$type: 'default',
showStepNumbers: true,
showStepTitles: true,
},
};Creating a Custom Stepper
Custom steppers use compound components with a local context pattern.
Step 1: Set Up Context and Provider
// my-stepper/context.ts
import { createContext, useContext } from 'react';
import { useStepperContext as useGlobalStepperContext } from '@schema-engine/forms-react/contexts/stepper-context';
interface MyStepperContextValue {
steps: StepData[];
currentStepIndex: number;
navigation: StepperNavigation;
form: FormData;
config: MyStepperConfig;
}
const MyStepperContext = createContext<MyStepperContextValue | null>(null);
export function MyStepperProvider({ children, config }: { children: React.ReactNode; config: MyStepperConfig }) {
const globalContext = useGlobalStepperContext();
return (
<MyStepperContext.Provider value={{ ...globalContext, config }}>
{children}
</MyStepperContext.Provider>
);
}
export function useMyStepperContext() {
const context = useContext(MyStepperContext);
if (!context) {
throw new Error('useMyStepperContext must be used within MyStepperProvider');
}
return context;
}Step 2: Create Compound Components
// my-stepper/components.tsx
import { useMyStepperContext } from './context';
function StepIndicator() {
const { steps, currentStepIndex } = useMyStepperContext();
return (
<div className="step-indicator">
{steps.map((step, index) => (
<div
key={step.id}
className={`step ${index === currentStepIndex ? 'active' : ''} ${step.isCompleted ? 'completed' : ''}`}
>
<span className="step-number">{index + 1}</span>
<span className="step-title">{step.title}</span>
</div>
))}
</div>
);
}
function NavigationButtons() {
const { navigation, form } = useMyStepperContext();
return (
<div className="navigation">
<button
onClick={navigation.goToPreviousStep}
disabled={navigation.isFirstStep}
>
Previous
</button>
<button
onClick={navigation.goToNextStep}
disabled={form.isSubmitting}
>
{navigation.isLastStep ? 'Submit' : 'Next'}
</button>
</div>
);
}Step 3: Create Main Stepper Component
// my-stepper/index.tsx
import { MyStepperProvider } from './context';
import { StepIndicator, NavigationButtons } from './components';
interface MyStepperConfig {
$type: 'my-stepper';
showNumbers?: boolean;
}
export function MyStepper({ config }: { config: MyStepperConfig }) {
return (
<MyStepperProvider config={config}>
<div className="my-stepper">
<StepIndicator />
<NavigationButtons />
</div>
</MyStepperProvider>
);
}Step 4: Create Registration
import { z } from 'zod';
import type { StepperRegistration } from '@schema-engine/forms';
const MyStepperConfigSchema = z.object({
$type: z.literal('my-stepper'),
showNumbers: z.boolean().optional().default(true),
});
export const MyStepperRegistration: StepperRegistration = {
$type: 'my-stepper',
schema: MyStepperConfigSchema,
metadata: {
name: 'My Stepper',
description: 'Custom navigation stepper',
},
render: MyStepper,
};Step 5: Register with Engine
const engine = createReactFormComposer()
.addStepper(MyStepperRegistration)
.build();Stepper Context
The global useStepperContext hook provides:
interface StepperContextValue {
steps: StepData[];
currentStepIndex: number;
navigation: StepperNavigation;
form: {
id: string;
title?: string;
description?: string;
isSubmitting?: boolean;
};
config: Record<string, any>;
}
interface StepData {
id: string;
title?: string;
description?: string;
isCompleted: boolean;
isValid: boolean;
isVisited: boolean;
}
interface StepperNavigation {
canGoNext: boolean;
canGoPrevious: boolean;
isFirstStep: boolean;
isLastStep: boolean;
goToStep: (index: number) => void;
goToNextStep: () => void;
goToPreviousStep: () => void;
}