We use cookies to ensure you get the best experience on our website. Read our privacy policy

Skip to main content

Directory

Multi Step Form Component

The Multi Step Form component provides a powerful foundation for building complex, multi-step forms with built-in validation, data persistence, and smooth animations. Perfect for surveys, wizards, and complex data collection workflows.

  • Check Circle

    Step Management: Flexible step configuration with back/forward navigation

  • Check Circle

    Data Persistence: Optional auto-save functionality with customizable storage and form flow history

  • Check Circle

    Validation: Built-in Zod schema validation with custom validation support

  • Check Circle

    Dynamic Step Management: Customize form flow through the schema, conditional steps made easy

The Multi Step Form requires configuration across several files to work properly:

First, define your form’s schema and steps in a dedicated file (e.g., src/schemas/form-flows/your-form.ts)

schemas/form-flows/your-form.ts
import { z } from 'zod';
export const flatSchema = z.object({
field1: z.string().min(2),
field2: z.string().email(),
field3: z.string(),
});

The schema here will be used to validate the form data in the Astro action and local storage.

Use the defineFormSchema function to define your form’s steps. The function accepts an object where the keys are the step IDs and the values are the step configurations. The file should be in src/lib/client/form-flows/your-form.ts

Each step configuration is defined using the defineStep function. This function accepts a configuration object that describes the step’s schema, validation, and other options.

Required Props
defineStep Arguments
Prop Type Description
schema ZodSchema The Zod schema for the step's fields
onComplete (formData: FormData) => Promise<string | undefined> Handler called when step is complete, returns next step ID
showSubmitButton boolean Show submit button instead of next
changeOnSelect boolean Advance to next step on selection
selectHandler (form: HTMLElement) => selectHandlerResult | null Custom changeOnSelect behavior handler
processInput Record<string, (value: string) => Promise<unknown> | unknown> Pre-process field values before validation
validate Record<string, (formData: FormData) => Promise<{ success: boolean, errors?: string[] }>> Custom validation functions
onReturn (form: HTMLFormElement) => Promise<void> Handler called when returning to step
onEnter (form: HTMLFormElement) => Promise<void> Handler called when entering step
lib/client/form-flows/your-form.ts
import { defineFormSchema, defineStep } from '@/schemas/multistep';
import { flatSchema } from './basic-schema';
export const yourFormSteps = defineFormSchema({
stepOne: defineStep({
schema: flatSchema.pick({
field1: true,
field2: true,
}),
async onComplete(formData) {
if (formData.get('field1') === 'helloworld') {
return 'secretStep'; // Return next step ID
}
return 'stepOne'; // Return same step ID
},
}),
stepTwo: defineStep({
schema: flatSchema.pick({
field3: true,
}),
showSubmitButton: true,
async onComplete() {
return undefined; // Form complete
},
}),
secretStep: defineStep({
schema: flatSchema.pick({
field3: true,
}),
showSubmitButton: true,
async onComplete() {
return undefined; // Form complete
},
}),
});

The selectHandler option is used to handle the selection for the step when using the changeOnSelect option. It should return an object with the following properties:

Required Props
selectHandler
Prop Type Description
getData () => FormData A function that is called to get the data for the step. It should return a `FormData` object.
subscribe (callback: (value?: string) => void) => void A function that is called to setup any event listeners to the data for the step. It's argument is a callback function that is called when the data changes via the event listener.
cleanup () => void A function that is called to cleanup the event listeners for the step.
schemas/form-flows/your-form.ts
import { defineFormSchema, defineStep } from '@/schemas/multistep';
import { flatSchema } from './basic-schema';
export const yourFormSteps = defineFormSchema({
stepOne: defineStep({
schema: flatSchema.pick({
field1: true,
field2: true,
}),
async onComplete(formData) {
if (formData.get('field1') === 'helloworld') {
return 'secretStep'; // Return next step ID
}
return 'stepOne'; // Return same step ID
},
}),
stepTwo: defineStep({
schema: flatSchema.pick({
field3: true,
}),
showSubmitButton: true,
async onComplete() {
return undefined; // Form complete
},
}),
secretStep: defineStep({
schema: flatSchema.pick({
field3: true,
}),
showSubmitButton: true,
async onComplete() {
return undefined; // Form complete
},
}),
});

Add your form’s schema to schemas/local-data.ts to enable data persistence:

schemas/local-data.ts
import { z } from 'zod';
import { flatSchema } from './basic-schema';
export const ZLocalDataSchemas = z.object({
// ... existing schemas ...
['your-form']: flatSchema,
});

Create your form page component (e.g., pages/your-form.astro) and use the MultistepForm component for each step.

pages/your-form.astro
---
import MSForm from '@/components/NST-components/UI/multistep-form/MultistepForm.astro';
import MSFormSection from '@/components/NST-components/UI/multistep-form/FormSection.astro';
import Input from '@/components/NST-components/UI/input/Input.astro';
---
<MSForm
name="your-form"
animation={{ type: 'slide', direction: 'horizontal' }}
scrollToTop
>
<MSFormSection name="stepOne">
<Input type="text" name="field1" required />
<Input type="email" name="field2" required />
</MSFormSection>
<MSFormSection name="secretStep">
<Input type="text" name="field3" required />
</MSFormSection>
<MSFormSection name="stepTwo">
<Input type="text" name="field3" required />
</MSFormSection>
</MSForm>

Create your form page script (e.g., scripts/your-form.ts):

scripts/your-form.ts
<script>
//eslint-disable-next-line
// @ts-nocheck
import { yourFormSteps } from './basic-steps';
import { ready } from '@/components/NST-components/base';
import { genericSaveDataHandler } from '@/lib/client/storage';
import { ActionError } from 'astro:actions';
async function init() {
const form = await ready(
document.querySelector('multistep-form')
);
if (!form) return;
form.setStepConfig({
config: yourFormSteps,
firstStep: 'stepOne',
localDataKey: 'your-form',
});
form.submitHandler = async (formData) => {
try {
await submitFormData(formData);
return undefined;
} catch {
return new ActionError({
message: 'Submission failed',
code: 'SUBMIT_ERROR',
});
}
};
// Optional: Enable auto-save
form.saveDataHandler = genericSaveDataHandler('your-form');
}
init();
</script>

Implement auto-save functionality with debouncing. You can set the debounce rate in milliseconds, and define the function that runs on form auto-save. There’s three ways to set this up.

Custom Auto-save
import { ready } from '@/components/NST-components/base';
import type { MSFElement } from '@/components/NST-components/UI/multistep-form/data';
import { getData, setData } from '@/lib/client/storage';
const contactForm = await ready(
document.querySelector<MSFElement>('multistep-form[name="contact-form"]')
);
if (contactForm) {
contactForm.saveDataHandler = (data) => {
const contactFormData = {
name: data.get('name')?.toString(),
email: data.get('email')?.toString(),
message: data.get('message')?.toString(),
};
setData('contact-form', {
...getData('contact-form'),
...contactFormData,
});
};
//If youre not saving all the data, you can disable the step history so the user will start on the first step if the page refreshes. But the data will still be saved.
contactForm.saveStepHistory = false;
}
Local Data Key
import { ready } from '@/components/NST-components/base';
import type { MSFElement } from '@/components/NST-components/UI/multistep-form/data';
const localDataKey = 'contact-form';
const contactForm = await ready(
document.querySelector<MSFElement>('multistep-form[name="contact-form"]')
);
if (contactForm) {
//Can also be set in the forms component props without needing a script
contactForm.localDataKey = localDataKey;
}
Generic Auto-save
import { ready } from '@/components/NST-components/base';
import type { MSFElement } from '@/components/NST-components/UI/multistep-form/data';
import { genericSaveDataHandler } from '@/lib/client/storage';
const localDataKey = 'contact-form';
const contactForm = await ready(
document.querySelector<MSFElement>('multistep-form[name="contact-form"]')
);
if (contactForm) {
contactForm.saveDataHandler = genericSaveDataHandler(localDataKey, {
dontSave: ['add_to_newsletter'],
});
contactForm.saveStepHistory = false;
}

The Form component provides the following functions:

MultistepForm Functions
Name Type Description
get: data () => FormData A function that is called to get the data for the form.
set: stepConfig () => void A function that is called to setup the step config for the form.
set: saveDataHandler (data: { data: Record<string, string | File>; stepHistory: string[]; currentStep: string; }) => void A function that is called to setup the save data handler for the form.
set: submitHandler (formData: FormData) => Promise<ActionError | undefined> A function that is called to submit the form.

Enable auto-save and form flow history functionality:

Auto-save Handler
form.saveDataHandler = (data) => {
localStorage.setItem('form-data', JSON.stringify(data));
};

Add custom validation to any step:

Custom Validation
defineStep({
schema: z.object({
username: z.string(),
}),
validate: {
username: async (formData) => {
const username = formData.get('username');
const isAvailable = await checkUsernameAvailability(username);
return {
success: isAvailable,
errors: isAvailable ? undefined : ['Username already taken'],
};
},
},
});

Create interactive selection steps:

Select Handler
defineStep({
schema: z.object({
plan: z.enum(['basic', 'pro', 'enterprise'])
}),
changeOnSelect: true, // Advance on selection
selectHandler: (form) => ({
subscribe: (callback) => {
form.addEventListener('click', (e) => {
const plan = e.target.closest('[data-plan]')?.dataset.plan;
if (plan) callback(plan);
});
},
cleanup: () => {
// Cleanup event listeners
}
})
Required Props
MultistepForm Extends <div> HTMLElement
Prop Type Default Description
name string - Unique identifier for the form
animation { type: "fade" | "slide", direction: "horizontal" | "vertical", duration: number } { type: "fade", direction: "horizontal", duration: 300 } Animation configuration
scrollToTop boolean false Scroll to top on step change
submitButtonText string Submit Submit button text
submitButtonTextSubmitting string Submitting... Text during submission
submitButtonProps ComponentProps<typeof Button> {} Submit button props
backButtonProps ComponentProps<typeof Button> {} Back button props
nextButtonProps ComponentProps<typeof Button> {} Next button props

The FormSection element is used to wrap the form fields for each step. It extends the <form> element and is used to wrap the form fields for each step.

Required Props
FormSection Extends <form> HTMLElement
Prop Type Default Description
name string - Unique identifier for the form

Limited Time Launch Sale

Get started with our Multi Step Form component and other powerful UI components in our AstroJS starter template!

GET 60% OFF!
North Star Themes Logo

Subscribe to our newsletter

Get the latest AstroJS tips and tricks right to your inbox

Email: [email protected]

© 2025 North Star Themes

Web Kit Provided By North Star Themes