New hire
Set up Priya's record before her first day at Northwind.
- Job detailsIn progress
- Personal detailsUp next
- Bank detailsUp next
- ReviewUp next
multistep-form.tsx
"use client";
import { useState, type SyntheticEvent } from "react";
import { LuArrowLeft, LuArrowRight, LuCheck } from "react-icons/lu";
import { Alert, Button, Input, Select, SelectOption, Stepper } from "@nexera-ui/react";
// Sample data
const TEAMS = [
"Design",
"Engineering",
"Finance",
"People Ops",
"Product",
"Customer",
] as const;
const NAMES = ["Job details", "Personal details", "Bank details", "Review"];
type Values = Record<
"title" | "team" | "start" | "name" | "email" | "birthday" | "holder" | "iban",
string
>;
const LABELS: Record<keyof Values, string> = {
title: "Job title",
team: "Team",
start: "Start date",
name: "Legal name",
email: "Personal email",
birthday: "Date of birth",
holder: "Account holder",
iban: "IBAN",
};
const STEP_FIELDS: (keyof Values)[][] = [
["title", "team", "start"],
["name", "email", "birthday"],
["holder", "iban"],
];
/** Multi-step form: a new-hire form split into steps, with a stepper you can use to go back. */
export default function MultiStepForm() {
const [current, setCurrent] = useState(1);
const [done, setDone] = useState(false);
const [values, setValues] = useState<Values>({
title: "Payroll analyst",
team: "People Ops",
start: "2026-11-02",
name: "",
email: "",
birthday: "",
holder: "",
iban: "",
});
// Completed steps are links back; upcoming ones are not reachable before the current step validates.
const steps = NAMES.map((label, index) => ({
label,
...(index + 1 < current && !done
? {
onPress: () => {
setCurrent(index + 1);
},
}
: {}),
}));
const field = (key: keyof Values) => ({
label: LABELS[key],
name: key,
value: values[key],
onChange: (value: string) => {
setValues((v) => ({ ...v, [key]: value }));
},
isRequired: true,
});
const onSubmit = (event: SyntheticEvent<HTMLFormElement>) => {
event.preventDefault(); // the browser only submits once the visible step is valid
if (current < NAMES.length) setCurrent(current + 1);
else setDone(true); // replace with your create-hire request
};
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<div className="mx-auto grid max-w-3xl gap-6 rounded-xl border border-default bg-surface p-5 shadow-sm @3xl:p-8">
<header className="grid gap-1">
<h2 className="text-heading-h3 text-primary">New hire</h2>
<p className="text-body-default text-secondary">
Set up Priya's record before her first day at Northwind.
</p>
</header>
<Stepper
aria-label="New hire progress"
current={done ? NAMES.length + 1 : current}
steps={steps}
/>
{done ? (
<Alert
tone="success"
title="New hire created"
description="Priya gets her welcome email on 26 October."
politeness="polite"
/>
) : (
<form className="grid gap-5" onSubmit={onSubmit} aria-labelledby="step-title">
<h3 id="step-title" className="text-heading-h4 text-primary">
Step {current} of {NAMES.length}: {NAMES[current - 1]}
</h3>
{current === 1 ? (
<div className="grid gap-4 @3xl:grid-cols-2">
<Input
{...field("title")}
autoComplete="organization-title"
errorMessage="Enter the job title."
/>
<Select
label="Team"
name="team"
value={values.team}
onChange={(key) => {
setValues((v) => ({ ...v, team: String(key) }));
}}
isRequired
>
{TEAMS.map((team) => (
<SelectOption key={team} id={team} label={team} />
))}
</Select>
<Input
{...field("start")}
type="date"
errorMessage="Choose the first working day."
/>
</div>
) : null}
{current === 2 ? (
<div className="grid gap-4 @3xl:grid-cols-2">
<Input
{...field("name")}
autoComplete="name"
description="As shown on their passport."
errorMessage="Enter their legal name."
/>
<Input
{...field("email")}
type="email"
autoComplete="email"
errorMessage="Enter an email address like name@example.com."
/>
<Input
{...field("birthday")}
type="date"
autoComplete="bday"
errorMessage="Enter their date of birth."
/>
</div>
) : null}
{current === 3 ? (
<div className="grid gap-4 @3xl:grid-cols-2">
<Input
{...field("holder")}
autoComplete="cc-name"
errorMessage="Enter the name on the bank account."
/>
<Input
{...field("iban")}
autoComplete="off"
placeholder="GB33 BUKB 2020 1555 5555 55"
errorMessage="Enter an IBAN, for example GB33 BUKB 2020 1555 5555 55."
/>
</div>
) : null}
{current === 4 ? (
<dl className="grid gap-3 rounded-lg border border-subtle bg-sunken p-4 @3xl:grid-cols-2">
{STEP_FIELDS.flat().map((key) => (
<div key={key} className="grid gap-0.5">
<dt className="text-body-small text-secondary">{LABELS[key]}</dt>
<dd className="text-body-strong break-words text-primary">
{values[key] || "Not given"}
</dd>
</div>
))}
</dl>
) : null}
<div className="flex flex-wrap justify-between gap-3">
<Button
variant="secondary"
size="md"
leadingIcon={<LuArrowLeft />}
isDisabled={current === 1}
onPress={() => {
setCurrent(Math.max(1, current - 1));
}}
>
Back
</Button>
<Button
type="submit"
size="md"
trailingIcon={current === NAMES.length ? <LuCheck /> : <LuArrowRight />}
>
{current === NAMES.length ? "Create new hire" : "Next"}
</Button>
</div>
</form>
)}
</div>
</div>
);
}
More forms and data entry blocks
- Contact formName, email, topic and message with validation.
- Add a personCreate a record in a modal.
- Address and phoneCountry, address lookup and phone with dialling code.
- File uploadDrag files in and watch each one upload.
- Date range pickerChoose a start and end date on a calendar.
- Search with suggestionsType ahead and pick from matching results.
- Terms and consentScrollable terms with a required checkbox.
- Choose an optionPick one option from a short list in a dialog.
- Pick a colourBrand or label colour from swatches or a picker.