AniUI Academy

Build a multi-step form wizard

mediumState & Data+220 XP

Build a three-step onboarding wizard: Account (email, password), Profile (name, role), Review (read-back + Submit).

The Next button advances only when the current step is valid. The Back button goes to the previous step without re-validating. State is preserved when navigating back.

Submit on the Review step calls the local handler and shows a confirmation.

Requirements

  • An element `<p data-testid="step-indicator">Step N of 3</p>` always reflects the current step (N is 1-indexed).
  • Step 1 renders `<input data-testid="email">` and `<input data-testid="password">`. Email must contain an "@" character; password must be ≥ 6 characters. Next is enabled only when both are valid.
  • Step 2 renders `<input data-testid="name">` and `<input data-testid="role">`. Both must be non-empty.
  • Step 3 renders `<p data-testid="review-email">`, `<p data-testid="review-name">`, `<p data-testid="review-role">` showing the committed values, and a `<button data-testid="submit">Submit</button>`.
  • The Next button is `<button data-testid="next">Next</button>` with `disabled={!valid}`.
  • The Back button is `<button data-testid="back">Back</button>`, hidden on step 1.
  • On submit, show `<p data-testid="done">Welcome, {name}!</p>` and hide the form.

Where it's been asked

  • Stripe
  • Shopify
  • Atlassian
  • LinkedIn

From public interview reports — a build very like this one has shown up in a round at these companies. Not an exhaustive list.

Stuck? The react course covers the ideas this problem is built on.

import { useState } from "react";

export default function App() {
  const [step, setStep] = useState(1);
  const [form, setForm] = useState({ email: "", password: "", name: "", role: "" });
  const [submitted, setSubmitted] = useState(false);

  // TODO: compute validity per step; wire Next (disabled when invalid),
  // Back (hidden on step 1), and the review step.

  if (submitted) {
    return <p data-testid="done">Welcome, {form.name}!</p>;
  }

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16, maxWidth: 320 }}>
      <p data-testid="step-indicator">Step {step} of 3</p>
      {/* TODO: render each step's inputs */}
      <div style={{ marginTop: 16 }}>
        {step > 1 ? <button data-testid="back">Back</button> : null}
        {step < 3 ? <button data-testid="next">Next</button> : null}
        {step === 3 ? <button data-testid="submit">Submit</button> : null}
      </div>
    </div>
  );
}

Ready to check it?

5 tests run against your rendered component, right here in your browser. Sign in to claim the XP when you pass.

Need a nudge?

Reveal a conceptual hint or the reference solution. Both were written and reviewed with the problem — no runtime magic.

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.