The FormWizard component turns one long form into a few short steps. It draws the numbered progress across the top, moves between steps with Back and Next, and checks the fields of a step before it lets the user move on. Every step stays in the page, so nothing typed is ever lost.
Example
Import both components and put a FormStep for each step inside the FormWizard. Give every step a title, which is the text under its number.
The first step.
The second step.
The last step: Next becomes Finish.
<script>
import { FormWizard, FormStep } from "theui-svelte";
</script>
<FormWizard>
<FormStep title="Account">...</FormStep>
<FormStep title="Profile">...</FormStep>
<FormStep title="Done">...</FormStep>
</FormWizard>With a Form
Put the wizard inside a Form and use the fields you already know. Before it moves on, the wizard checks the HTML validation of the fields in the current step, so required, type="email" and the rest are enough for most cases. Try pressing Next with the field empty.
step: 1
<script>
let step = $state(1)
</script>
<Form method="POST">
<FormWizard bind:step onfinish={() => save()}>
<FormStep title="Account" description="How you sign in">
<Input name="email" type="email" required>Email</Input>
</FormStep>
<FormStep title="Profile" description="About you">
<Input name="fullName" required>Full name</Input>
</FormStep>
<FormStep title="Done">Everything is ready.</FormStep>
</FormWizard>
</Form>Step Validation
A step can also check itself with its own validate function. Return false to keep the user where they are, or true to let them through. The function may be async, so a check on your server works too.
Next only works once this is ticked.
Thank you.
<script>
let accepted = $state(false)
</script>
<FormWizard>
<FormStep title="Terms" validate={() => accepted}>
<Checkbox bind:checked={accepted}>I accept the terms</Checkbox>
</FormStep>
<FormStep title="Welcome">Thank you.</FormStep>
</FormWizard>
<!-- An async check works as well -->
<FormStep title="Username" validate={async () => await isFree(username)}>...</FormStep>Vertical Steps
Set orientation="vertical" to put the steps down the side. On a narrow screen they stack above the content by themselves.
Plans go here.
Check and finish.
<FormWizard orientation="vertical">
<FormStep title="Pick a plan" description="Monthly or yearly">...</FormStep>
<FormStep title="Payment" description="Card details">...</FormStep>
<FormStep title="Review">...</FormStep>
</FormWizard>Free Movement
By default the wizard is linear: a step opens only once the ones before it are done. Pass linear=false to let the user jump to any step from the numbers at the top, which suits a form they are coming back to.
Any number can be clicked.
Even before the earlier steps are done.
This step is marked optional.
<FormWizard linear={false} step={2}>
<FormStep title="One">...</FormStep>
<FormStep title="Two">...</FormStep>
<FormStep title="Three" optional>...</FormStep>
</FormWizard>Your Own Controls
Turn the built-in buttons off with controls=false, or the numbers at the top off with header=false, and drive the wizard through bind:step. The button labels can be reworded with backText, nextText and finishText.
Use the buttons below.
Step two.
<script>
let step = $state(1)
</script>
<FormWizard bind:step controls={false}>
<FormStep title="First">...</FormStep>
<FormStep title="Second">...</FormStep>
</FormWizard>
<button onclick={() => step--}>Previous</button>
<button onclick={() => step++}>Continue</button>
<!-- Or keep the buttons and rename them -->
<FormWizard backText="Go back" nextText="Continue" finishText="Create account">...</FormWizard>Customization
Use headerClasses for the row of steps, indicatorClasses for every number, and activeIndicatorClasses and completeIndicatorClasses for the current and the finished ones. stepClasses reaches every step, and controlsClasses and buttonClasses style the buttons.
Square numbers and pill buttons.
Finished steps turn green.
<FormWizard
indicatorClasses="rounded-lg"
activeIndicatorClasses="ring-4 ring-brand-500/20"
completeIndicatorClasses="bg-success-500 border-success-500"
buttonClasses="rounded-full px-6"
>
...
</FormWizard>Accessibility
The steps are an ordered list, so a screen reader hears how many there are and which one is current, marked with aria-current="step". A finished step says so in its name, and a step that cannot be reached yet is a disabled button rather than a silent one. Each step is a group named by its title, and moving to a step puts focus on it, so the reading position follows the visible one. Steps that are not shown stay in the page but are hidden and made inert, which keeps their values and keeps them out of the tab order. When a field fails its check, the browser's own message is shown on that field and the wizard stays where it is.