Form Wizard

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.

Svelte
<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.

Press Next while this is empty

step: 1

Svelte
<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.

Svelte
<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.

Svelte
<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.

Svelte
<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.

Svelte
<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.

Svelte
<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.

Configuration

FormWizard

FormStep