Onboarding
Two-step account wizard
Validated fields gate the next button
Validated fields gate the next button
Build a two-step signup wizard with Tailwind CSS. Centre a max-w-lg card. The header shows a step counter reading "Step 1 of 2" with a thin two-segment progress bar under it. Step one collects a full name, a work email and a password with a hint line about the minimum length; step two collects a company name, a team size select and a checkbox agreeing to terms. Only one step is visible at a time. The footer row has a ghost Back button, disabled on the first step, and a dark Next button that becomes Create account on the last step. Then add JavaScript that validates the visible step on input — a non-empty name, an email containing an at sign and a dot, a password of at least ten characters, and the checkbox on step two — enabling the primary button only when the step is valid, showing a small red message under any field left invalid after it has been touched, advancing the progress segments, and replacing the card contents with a success state on submit. <section class="flex justify-center bg-neutral-50 px-6 py-16">
<div id="wizardCard" class="w-full max-w-lg rounded-2xl border border-neutral-200 bg-white p-8 shadow-sm">
<div class="mb-7">
<p id="wizardStepLabel" class="text-xs font-medium uppercase tracking-widest text-neutral-400">Step 1 of 2</p>
<div class="mt-3 flex gap-1.5">
<span class="wizard-seg h-1 flex-1 rounded-full bg-neutral-900"></span>
<span class="wizard-seg h-1 flex-1 rounded-full bg-neutral-200"></span>
</div>
</div>
<div class="wizard-step" data-step="1">
<h2 class="text-xl font-semibold tracking-tight text-neutral-900">Create your account</h2>
<p class="mt-1 text-sm text-neutral-500">Two short steps and you are in.</p>
<div class="mt-6 space-y-4">
<div>
<label class="mb-1.5 block text-xs font-medium text-neutral-700">Full name</label>
<input id="fName" type="text" placeholder="Rae Mensah" class="w-full rounded-xl border border-neutral-200 px-3.5 py-2.5 text-sm outline-none transition focus:border-neutral-900 focus:ring-4 focus:ring-neutral-900/10" />
<p class="err mt-1.5 hidden text-xs text-red-600">Tell us what to call you.</p>
</div>
<div>
<label class="mb-1.5 block text-xs font-medium text-neutral-700">Work email</label>
<input id="fEmail" type="email" placeholder="[email protected]" class="w-full rounded-xl border border-neutral-200 px-3.5 py-2.5 text-sm outline-none transition focus:border-neutral-900 focus:ring-4 focus:ring-neutral-900/10" />
<p class="err mt-1.5 hidden text-xs text-red-600">That does not look like an email address.</p>
</div>
<div>
<label class="mb-1.5 block text-xs font-medium text-neutral-700">Password</label>
<input id="fPassword" type="password" placeholder="••••••••••" class="w-full rounded-xl border border-neutral-200 px-3.5 py-2.5 text-sm outline-none transition focus:border-neutral-900 focus:ring-4 focus:ring-neutral-900/10" />
<p class="mt-1.5 text-xs text-neutral-400">At least 10 characters. A passphrase beats a puzzle.</p>
<p class="err mt-1.5 hidden text-xs text-red-600">Ten characters minimum.</p>
</div>
</div>
</div>
<div class="wizard-step hidden" data-step="2">
<h2 class="text-xl font-semibold tracking-tight text-neutral-900">About your team</h2>
<p class="mt-1 text-sm text-neutral-500">This shapes the defaults we set up for you.</p>
<div class="mt-6 space-y-4">
<div>
<label class="mb-1.5 block text-xs font-medium text-neutral-700">Company</label>
<input id="fCompany" type="text" placeholder="Northwind" class="w-full rounded-xl border border-neutral-200 px-3.5 py-2.5 text-sm outline-none transition focus:border-neutral-900 focus:ring-4 focus:ring-neutral-900/10" />
<p class="err mt-1.5 hidden text-xs text-red-600">A company or project name, whichever fits.</p>
</div>
<div>
<label class="mb-1.5 block text-xs font-medium text-neutral-700">Team size</label>
<select id="fSize" class="w-full rounded-xl border border-neutral-200 bg-white px-3.5 py-2.5 text-sm outline-none transition focus:border-neutral-900">
<option>Just me</option><option>2 – 10</option><option>11 – 50</option><option>51 – 200</option><option>More than 200</option>
</select>
</div>
<label class="flex items-start gap-2.5 text-sm text-neutral-600">
<input id="fTerms" type="checkbox" class="mt-0.5 rounded border-neutral-300" />
<span>I agree to the <a href="#" class="underline underline-offset-2">terms of service</a> and <a href="#" class="underline underline-offset-2">privacy policy</a>.</span>
</label>
<p id="termsErr" class="err hidden text-xs text-red-600">We need this one before we can continue.</p>
</div>
</div>
<div class="mt-8 flex items-center justify-between border-t border-neutral-200 pt-5">
<button id="wizardBack" disabled class="rounded-xl px-4 py-2.5 text-sm font-medium text-neutral-400 transition disabled:cursor-not-allowed">Back</button>
<button id="wizardNext" disabled class="cursor-not-allowed rounded-xl bg-neutral-900 px-6 py-2.5 text-sm font-medium text-white opacity-40 transition">Next</button>
</div>
</div>
</section> var card = document.getElementById("wizardCard");
var steps = Array.prototype.slice.call(document.querySelectorAll(".wizard-step"));
var segs = Array.prototype.slice.call(document.querySelectorAll(".wizard-seg"));
var label = document.getElementById("wizardStepLabel");
var back = document.getElementById("wizardBack");
var next = document.getElementById("wizardNext");
var step = 1;
var touched = {};
var RULES = {
fName: function (v) { return v.trim().length > 0; },
fEmail: function (v) { return v.indexOf("@") > 0 && v.lastIndexOf(".") > v.indexOf("@"); },
fPassword: function (v) { return v.length >= 10; },
fCompany: function (v) { return v.trim().length > 0; },
};
function fieldsFor(n) {
return n === 1 ? ["fName", "fEmail", "fPassword"] : ["fCompany"];
}
function valid(n) {
var ok = fieldsFor(n).every(function (id) {
return RULES[id](document.getElementById(id).value);
});
if (n === 2) ok = ok && document.getElementById("fTerms").checked;
return ok;
}
function paintErrors() {
fieldsFor(step).forEach(function (id) {
var input = document.getElementById(id);
var err = input.parentElement.querySelector(".err");
// Only complain about a field the person has already left.
var show = touched[id] && !RULES[id](input.value);
err.classList.toggle("hidden", !show);
input.classList.toggle("border-red-400", show);
});
if (step === 2) {
var terms = document.getElementById("fTerms");
document.getElementById("termsErr").classList.toggle("hidden", !(touched.fTerms && !terms.checked));
}
}
function sync() {
var ok = valid(step);
next.disabled = !ok;
next.classList.toggle("opacity-40", !ok);
next.classList.toggle("cursor-not-allowed", !ok);
next.textContent = step === steps.length ? "Create account" : "Next";
back.disabled = step === 1;
back.classList.toggle("text-neutral-400", step === 1);
back.classList.toggle("text-neutral-700", step > 1);
back.classList.toggle("hover:bg-neutral-50", step > 1);
label.textContent = "Step " + step + " of " + steps.length;
segs.forEach(function (s, i) {
s.classList.toggle("bg-neutral-900", i < step);
s.classList.toggle("bg-neutral-200", i >= step);
});
steps.forEach(function (s) {
s.classList.toggle("hidden", Number(s.dataset.step) !== step);
});
paintErrors();
}
card.addEventListener("input", function (e) {
if (e.target.id) touched[e.target.id] = true;
sync();
});
card.addEventListener("change", function (e) {
if (e.target.id) touched[e.target.id] = true;
sync();
});
back.addEventListener("click", function () {
if (step > 1) step -= 1;
sync();
});
next.addEventListener("click", function () {
if (next.disabled) return;
if (step < steps.length) {
step += 1;
sync();
return;
}
card.innerHTML =
'<div class="py-8 text-center">' +
'<div class="mx-auto grid h-14 w-14 place-items-center rounded-full bg-green-100 text-2xl text-green-700">✓</div>' +
'<h2 class="mt-5 text-xl font-semibold text-neutral-900">Account created</h2>' +
'<p class="mt-2 text-sm text-neutral-500">Check your inbox for the verification link, then we will finish setting up your workspace.</p>' +
'<button class="mt-6 rounded-xl bg-neutral-900 px-6 py-2.5 text-sm font-medium text-white">Open the dashboard</button>' +
"</div>";
});
sync();