Multi-Step Forms
Create step-by-step form wizards with validation
Live Demo
Steps
API: Form.Steps
Break a long form into manageable steps:
<Form schema={Schema} initialValue={data} onSubmit={save}>
<Form.Steps animated validateOnNext>
<Form.Steps.Step title="Personal Info">
<Form.Field.String name="firstName" />
<Form.Field.String name="lastName" />
<Form.Field.String name="email" />
</Form.Steps.Step>
<Form.Steps.Step title="Address">
<Form.Field.String name="street" />
<Form.Field.String name="city" />
<Form.Field.String name="zip" />
</Form.Steps.Step>
<Form.Steps.Step title="Preferences">
<Form.Field.Checkbox name="newsletter" />
<Form.Field.Select name="language" />
</Form.Steps.Step>
<Form.Steps.Navigation />
</Form.Steps>
</Form>Full example
The complete sandbox example, read directly from the form-develop-app / form-develop-app-shadcn
source at build time. The shadcn side ships the same compound component as FormSteps (beta,
form-level, not a Field) with a smaller two-step schema — same mechanics, different scale.
'use client'
import { Box, Card, Container, Heading, Stack, Text, VStack } from '@chakra-ui/react'
import { Form } from '@letar/forms'
import { z } from 'zod/v4'
// Schema for multi-step form
const RegistrationSchema = z.object({
// Step 1: Personal info
firstName: z.string().min(2, 'Minimum 2 characters'),
lastName: z.string().min(2, 'Minimum 2 characters'),
birthDate: z.string().optional(),
// Step 2: Contact info
email: z.string().email('Invalid email'),
phone: z.string().optional(),
// Step 3: Account settings
username: z.string().min(3, 'Minimum 3 characters'),
notifications: z.boolean().default(true),
theme: z.enum(['light', 'dark', 'system']).default('system'),
})
type RegistrationData = z.infer<typeof RegistrationSchema>
const initialValues: RegistrationData = {
firstName: '',
lastName: '',
birthDate: '',
email: '',
phone: '',
username: '',
notifications: true,
theme: 'system',
}
export default function StepsDemoPage() {
const handleSubmit = (data: RegistrationData) => {
// eslint-disable-next-line no-console -- демо-приложение
console.log('Form submitted:', data)
alert('Registration complete!\n\n' + JSON.stringify(data, null, 2))
}
return (
<Container maxW="container.md" py={8}>
<VStack gap={8} align="stretch">
<Heading size="xl">Form.Steps Demo</Heading>
<Text color="fg.muted">Multi-step form with validation on each step</Text>
<Card.Root>
<Card.Body>
<Form initialValue={initialValues} schema={RegistrationSchema} onSubmit={handleSubmit}>
<Form.Steps validateOnNext linear colorPalette="brand" animated>
<Form.Steps.Indicator showDescriptions />
<Box my={6}>
<Form.Steps.Step title="Personal" description="Your details">
<Stack gap={4}>
<Heading size="md">Personal Information</Heading>
<Form.Field.String name="firstName" label="First Name" required />
<Form.Field.String name="lastName" label="Last Name" required />
<Form.Field.Date name="birthDate" label="Birth Date" />
</Stack>
</Form.Steps.Step>
<Form.Steps.Step title="Contact" description="How to reach you">
<Stack gap={4}>
<Heading size="md">Contact Information</Heading>
<Form.Field.String name="email" label="Email" required />
<Form.Field.String name="phone" label="Phone" />
</Stack>
</Form.Steps.Step>
<Form.Steps.Step title="Account" description="Settings">
<Stack gap={4}>
<Heading size="md">Account Settings</Heading>
<Form.Field.String name="username" label="Username" required />
<Form.Field.Switch name="notifications" label="Enable notifications" />
<Form.Field.RadioGroup
name="theme"
label="Theme"
options={[
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
{ value: 'system', label: 'System' },
]}
/>
</Stack>
</Form.Steps.Step>
<Form.Steps.CompletedContent>
<Stack gap={4} py={4}>
<Heading size="md" color="green.500">
All steps complete!
</Heading>
<Text>Review your information and click Submit to finish registration.</Text>
</Stack>
</Form.Steps.CompletedContent>
</Box>
{/* data-* на кнопках навигации: якоря для подсказок наставника и тестов (v2.14.22+) */}
<Form.Steps.Navigation
prevLabel="Back"
nextLabel="Continue"
submitLabel="Create Account"
prevProps={{ 'data-assist-id': 'steps-demo.prev' }}
nextProps={{ 'data-assist-id': 'steps-demo.next' }}
submitProps={{ 'data-assist-id': 'steps-demo.submit' }}
/>
</Form.Steps>
</Form>
</Card.Body>
</Card.Root>
{/* Non-linear example with animation */}
<Heading size="lg" mt={8}>
Non-linear Steps (with animation)
</Heading>
<Text color="fg.muted">Click on any step to navigate directly. Animation duration: 0.5s</Text>
<Card.Root>
<Card.Body>
<Form
initialValue={{ step1Field: '', step2Field: '', step3Field: '' }}
// eslint-disable-next-line no-console -- демо-приложение
onSubmit={(data) => console.log('Non-linear form:', data)}
>
<Form.Steps colorPalette="teal" animated animationDuration={0.5}>
<Form.Steps.Indicator />
<Box my={6}>
<Form.Steps.Step title="Step 1">
<Form.Field.String name="step1Field" label="Step 1 Field" />
</Form.Steps.Step>
<Form.Steps.Step title="Step 2">
<Form.Field.String name="step2Field" label="Step 2 Field" />
</Form.Steps.Step>
<Form.Steps.Step title="Step 3">
<Form.Field.String name="step3Field" label="Step 3 Field" />
</Form.Steps.Step>
</Box>
<Form.Steps.Navigation />
</Form.Steps>
</Form>
</Card.Body>
</Card.Root>
</VStack>
</Container>
)
}'use client'
import { FieldString, FormSteps } from '@letar/forms-shadcn'
import { DemoForm, DemoPageLayout } from '../_components'
export default function StepsDemoPage() {
return (
<DemoPageLayout
title="FormSteps (beta)"
description="Compound-компонент форм-уровня, не Field — изолированная песочница."
>
<DemoForm<{ firstName: string; email: string }>
defaultValues={{ firstName: '', email: '' }}
onSubmit={(value) => {
// eslint-disable-next-line no-console
console.log('steps submit', value)
}}
>
<FormSteps>
<FormSteps.Indicator showDescriptions />
<FormSteps.Step title="Личное" description="Как к вам обращаться">
<FieldString name="firstName" label="Имя" required />
</FormSteps.Step>
<FormSteps.Step title="Контакты" description="Как с вами связаться">
<FieldString name="email" label="Email" type="email" />
</FormSteps.Step>
<FormSteps.CompletedContent>
<p className="text-sm">Все шаги пройдены — можно отправлять.</p>
</FormSteps.CompletedContent>
{/* data-* на кнопках навигации: якоря для подсказок наставника и тестов */}
<FormSteps.Navigation
prevProps={{ 'data-assist-id': 'steps-demo.prev' }}
nextProps={{ 'data-assist-id': 'steps-demo.next' }}
submitProps={{ 'data-assist-id': 'steps-demo.submit' }}
/>
</FormSteps>
</DemoForm>
</DemoPageLayout>
)
}Vue-пример для этого поля появится позже.
Angular-пример для этого поля появится позже.
Features
animated— smooth transitions between stepsvalidateOnNext— validates current step before allowing navigationForm.Steps.Navigation— renders Previous / Next / Submit buttons- Step indicator — shows progress through steps
- Keyboard navigation — supports arrow keys
Navigation button props
prevProps, nextProps, submitProps and skipProps pass extra attributes to each button —
data-* (assistant anchors, data-testid, analytics), aria-* and other native button attributes.
data-* literals need no as:
<Form.Steps.Navigation
prevProps={{ 'data-assist-id': 'wizard.prev' }}
nextProps={{ 'data-assist-id': 'wizard.next' }}
submitProps={{ 'data-assist-id': 'wizard.submit' }}
/>- Next and Submit are two different buttons. On the last step
submitPropsapplies instead ofnextProps; attributes never move from one to the other. onClick,disabled,loadingandtypeare set by the component. They are not part of the prop type and are overridden at runtime, so step navigation cannot be broken through these props.- Available since
@letar/forms2.14.22; the same props exist in@letar/forms-shadcn,@letar/forms-vueand@letar/forms-vue-shadcn.
Completed screen
Form.Steps.CompletedContent renders a screen shown after the last step is confirmed — a summary
or "review before you submit" panel:
<Form.Steps validateOnNext>
<Form.Steps.Step title="Personal Info">...</Form.Steps.Step>
<Form.Steps.Step title="Address">...</Form.Steps.Step>
<Form.Steps.CompletedContent>
<p>All done! Review your data and click Submit.</p>
</Form.Steps.CompletedContent>
<Form.Steps.Navigation />
</Form.Steps>Its presence in the tree changes what the last step's button does (@letar/forms 2.16.0+):
Continue first moves into the completed state (where CompletedContent renders), and only there
does Form.Steps.Navigation switch to Submit. Without CompletedContent, the last step still
submits immediately — the completed screen is opt-in, existing forms are unaffected.
Step Persistence
Steps can be persisted to localStorage to survive page refreshes:
<Form.Steps animated validateOnNext persistKey="checkout-wizard">
...
</Form.Steps>