AZMARA PLATFORM
Examples

Onboarding wizard

A step-by-step form with a progress indicator, per-step validation and focus that follows the step.

View as Markdown for coding assistants, or see llms.txt.

Three steps that gather a team name and size, a plan, and a confirmation.

Preview theme
Preview density
  1. 1Current stepYour teamName and size
  2. 2Not startedYour planPick one
  3. 3Not startedConfirm

Your team

<form
  onSubmit={next}
  noValidate
  aria-label="Set up your team"
  className="flex max-w-xl flex-col gap-6"
>
  <Stepper steps={STEPS} current={step} label="Set-up progress" />
  <Card>
    <CardContent className="flex flex-col gap-4 pt-6">
      <h2
        key={step}
        ref={focusHeading}
        tabIndex={-1}
        className="m-0 text-lg font-semibold outline-none"
      >
        {STEPS[step]?.label}
      </h2>

      {error && (
        <Alert variant="destructive">
          <AlertDescription>{error}</AlertDescription>
        </Alert>
      )}

      {step === 0 && (
        <>
          <div className="flex flex-col gap-2">
            <label htmlFor={`${id}-team`} className="text-sm font-medium">
              Team name
            </label>
            <Input
              id={`${id}-team`}
              value={team}
              autoComplete="organization"
              onChange={(event) => setTeam(event.target.value)}
            />
          </div>
          <div className="flex flex-col gap-2">
            <label htmlFor={`${id}-size`} className="text-sm font-medium">
              How many people?
            </label>
            <Input
              id={`${id}-size`}
              inputMode="numeric"
              value={size}
              onChange={(event) => setSize(event.target.value)}
            />
          </div>
        </>
      )}

      {step === 1 && (
        <ToggleGroup
          aria-label="Plan"
          variant="outline"
          value={[plan]}
          onValueChange={(value) => {
            // One plan is always chosen: ignore the click that would turn the last one off.
            const chosen = (value as string[])[0];
            if (chosen) setPlan(chosen);
          }}
          className="flex-col items-stretch"
        >
          {PLANS.map((option) => (
            <ToggleGroupItem key={option.value} value={option.value} className="justify-start">
              {option.label}
            </ToggleGroupItem>
          ))}
        </ToggleGroup>
      )}

      {step === 2 && (
        <dl className="m-0 grid grid-cols-[8rem_1fr] gap-2 text-sm">
          <dt className="text-muted-foreground">Team</dt>
          <dd className="m-0">{team}</dd>
          <dt className="text-muted-foreground">People</dt>
          <dd className="m-0">{size}</dd>
          <dt className="text-muted-foreground">Plan</dt>
          <dd className="m-0">{plan}</dd>
        </dl>
      )}
    </CardContent>
    <CardFooter className="justify-between">
      <Button type="button" variant="ghost" disabled={step === 0} onClick={() => go(step - 1)}>
        Back
      </Button>
      <Button type="submit">{step === STEPS.length - 1 ? "Create team" : "Continue"}</Button>
    </CardFooter>
  </Card>
</form>
Open as text

What it shows

  • One source of truth. Every answer is held in the wizard's state, so Back never loses anything and the last step can show a summary.
  • Labelled fields. Each Input has a visible label. The headcount field asks for a numeric keyboard with inputMode="numeric" and is still validated as text, because a number input would hide what the person typed.
  • Validate a step before leaving it. Continue checks the current step only. An error appears in an Alert and the step does not change.
  • Focus follows the step. When the step changes, the new step's heading takes focus. Otherwise a keyboard or screen reader user is left on a button that has just been replaced. The heading has tabIndex={-1}, so it can be focused but is not in the tab order.
  • Progress that is announced. The Stepper marks the current step with aria-current="step" and each step's state is in its text, not only its colour.
  • A choice that cannot be empty. The plan is a single-choice ToggleGroup. Clicking the chosen plan again would turn it off, so the example ignores that click: a group that has to have an answer keeps one.

Limits

Reloading the page loses the answers. If that matters, keep the state in the URL or in storage and read it back. The final step does not call a server.