Next Stepper for React: Multi-Step Forms with Clear Progress

A long setup form is hard to finish when people cannot tell how much work remains, which step they are on, or whether going back will erase what they entered. If you searched for a “next stepper,” you probably need a progress indicator inside a Next.js flow, plus the state and validation that make the flow behave predictably.
A stepper is only one part of that interface. It can show progress, but it should not decide whether a form is valid, save an account, or submit data. Keep those jobs explicit in your app and pass the current step to the visual component.
A stepper shows position; your app owns the flow
Treat the step index as UI state, separate from the values the person enters. For a three-step project setup, the state might move through account details, project details, and a review screen. The form values should stay in a parent component or form store so changing the visible step does not discard them.
The progress indicator should describe that state. It should not be the state machine itself. This separation lets you change the validation rules, persist a draft, or route the final submission through a server action without replacing the visual stepper.
The OTF Stepper follows that boundary. Its required props are a numeric currentStep and a steps array. Each step has a title and may have a description; direction can be horizontal or vertical. It renders completed, current, and upcoming steps based on the index you provide. It does not own your fields, validation, navigation buttons, or submit handler.

Put interactive state behind a small client boundary
Next.js App Router pages are server components by default. A step flow that uses React state and button event handlers needs a client component. Keep the boundary at the interactive wizard rather than marking the whole route as client rendered. The Next.js use client documentation describes that directive as the entry point for components that need state management or event handling.
Here is a small client-side example. It keeps the active step in one state variable, keeps entered values in the same component, and passes a zero-based index to Stepper.
'use client'
import { useState, type FormEvent } from 'react'
import { Stepper } from '@otfdashkit/ui'
const steps = [
{ title: 'Account', description: 'Name your workspace' },
{ title: 'Project', description: 'Add the first project' },
{ title: 'Review', description: 'Check before creating' },
]
export function ProjectWizard() {
const [currentStep, setCurrentStep] = useState(0)
const [workspace, setWorkspace] = useState('')
const [project, setProject] = useState('')
const [reviewed, setReviewed] = useState(false)
const lastStep = steps.length - 1
const canContinue =
currentStep === 0 ? workspace.trim().length > 0 :
currentStep === 1 ? project.trim().length > 0 :
true
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
if (!canContinue) return
if (currentStep < lastStep) {
setCurrentStep(step => step + 1)
return
}
setReviewed(true)
}
if (reviewed) {
return <p role="status">Review complete. Send these values through your project action.</p>
}
return (
<section aria-labelledby="project-title">
<h1 id="project-title">Create a project</h1>
<Stepper currentStep={currentStep} steps={steps} />
<form onSubmit={handleSubmit}>
<div aria-live="polite">
<h2>{steps[currentStep].title}</h2>
<p>{steps[currentStep].description}</p>
</div>
{currentStep === 0 && (
<label>
Workspace name
<input
required
value={workspace}
onChange={event => setWorkspace(event.target.value)}
/>
</label>
)}
{currentStep === 1 && (
<label>
Project name
<input
required
value={project}
onChange={event => setProject(event.target.value)}
/>
</label>
)}
{currentStep === lastStep && (
<p>{workspace} / {project}</p>
)}
<button
type="button"
disabled={currentStep === 0}
onClick={() => setCurrentStep(step => Math.max(0, step - 1))}
>
Back
</button>
<button type="submit" disabled={!canContinue}>
{currentStep === lastStep ? 'Finish review' : 'Continue'}
</button>
</form>
</section>
)
}This example demonstrates client-side navigation only. In a real product, validate each field on the server as well, and send the final values to the action or route handler that owns project creation. The local check gives immediate feedback; it is not a substitute for server-side validation.
11 production screens. Login and database wired.
The SaaS Dashboard Kit includes login, a database, and a working dashboard. Live demo at saas.otf-kit.dev.
Validate before changing the visible step
A “Continue” button should advance only after the current step passes its rules. Start with the smallest useful checks: required values are present, email fields parse, and choices belong to the options you offer. Show an error beside the relevant field and associate it with that input so the reason is clear to keyboard and screen-reader users.
Do not validate every later field when someone is still on the first step. That produces errors for controls they have not reached. Conversely, do not mark a step complete just because the person clicked Continue. The step index should move only after the current validation succeeds.
Make Back predictable
Back should change the visible step without resetting values. In the example, workspace and project values live above the conditional step content, so they remain in state as each panel changes. If you mount a separate form for every step, confirm that the form library preserves unmounted fields or store the values outside those panels.
Also choose what happens when someone returns to a completed step and edits it. If changing an earlier answer affects later choices, either clear dependent values or recompute them. For example, changing the account type may invalidate a plan selected on the next screen. The indicator will still show later steps as complete when currentStep is ahead; your app should move the index back to the first step that needs review.

Do not let Back submit the form. It is a navigation control, so give it type="button". The forward control can submit the current form element, validate the step, and either advance or finish. On the final step, use a clear action such as “Create project” instead of another generic “Continue.”
Give every step a clear accessible context
A progress graphic does not explain the current task by itself. Keep a heading for the visible step, use a label for each input, and group related fields with fieldset and legend when that helps. Keep keyboard focus in a sensible place after a step change; for a longer panel, focusing its heading can orient the reader without forcing them to tab through the whole page again.
The OTF Stepper is a visual indicator made from step labels, numbers, and completed-state icons. Its currentStep prop controls styling, but the component does not manage form focus, validation errors, or announcements. Keep those behaviors in your form. If your design needs assistive technology to hear progress such as “Step 2 of 3,” add that message in your own interface and test it with the screen readers your product supports.
Preserve native button behavior and avoid disabling the only way to leave a step. If a previous action is unavailable, explain why in nearby text. On narrow screens, consider the vertical direction so labels and descriptions have room, but keep the same state and navigation rules.
Keep the route useful without JavaScript-heavy pages
The wizard itself needs client-side behavior, but the surrounding route can still render as a server component. Put static page copy, product data, and metadata outside the interactive component. Pass serializable values into the client boundary and avoid passing server-only functions as props. This makes the interactive part easier to reason about and limits the amount of code that must run in the browser.
If a step needs server data, fetch it in the route or a server component and pass it down. Send mutations through a server action or route handler and return errors the client can display. The stepper only reflects the position your flow reports.
Use the component for progress, not business rules
If the page already uses the OTF React SDK, the OTF Stepper component gives you a horizontal or vertical progress display from one import. Install the free package with:
bun add @otfdashkit/uiThen keep your step definitions beside the form flow and pass the active index into Stepper. If you need a different visual treatment, the component accepts className; the flow can keep the same state while you adjust presentation.
If your app needs a complete set of connected screens rather than one component, browse the OTF kits after previewing the Stepper page and the free SDK docs. A kit is a separate starting point for a full app; the Stepper package remains the smaller choice when you already own the route and data model. For a broader discussion of how a stable component API helps coding agents extend a codebase, read the UI contract guide.
Frequently asked questions
Does the OTF Stepper component control form values?
No. It displays the current position from props. Your form or client component owns the field values, validation, and submit behavior.
Is a Next.js stepper a server or client component?
The interactive flow needs a client boundary if it uses React state or event handlers. Keep the rest of the route on the server when it does not need browser interaction.
How should Back work in a multi-step form?
Move to the previous index while keeping entered values in state. If an earlier edit invalidates later answers, clear or revalidate those dependent values before showing the review step again.
Sources
Ship the product, not the setup.
- 11 production screens — auth, team, analytics, and settings
- Login and a real database are wired in
- AI configs pre-tuned so your agent extends instead of regenerates