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';
StepperDescription
defaultHorizontal progress indicator with step numbers
minimalistClean, minimal progress bar
debuggerDevelopment 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;
}