Form — Multi-step (progress + back/next)
A four-step signup wizard (Account, Profile, Plan, Review) with a numbered progress indicator and animated gradient bar. Each step validates its own fields before Next unlocks, Back preserves every value, and the Review screen summarizes all answers with per-row Edit jumps. Submit runs a real busy state into a success confirmation. Inline errors, aria-invalid, an aria-live error summary, password reveal, focus moved to the active step heading, and reduced-motion support throughout.
MCP
Code
:root {
--brand: #5b5bf0;
--brand-d: #4646d6;
--brand-700: #3a3ab8;
--brand-50: #eef0ff;
--accent: #00b4a6;
--accent-soft: #d8f5f2;
--ink: #101322;
--ink-2: #3a4060;
--muted: #6c7393;
--bg: #f6f7fb;
--white: #ffffff;
--surface: #ffffff;
--line: rgba(16, 19, 34, 0.1);
--line-2: rgba(16, 19, 34, 0.16);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(16, 19, 34, 0.08);
--sh-2: 0 8px 24px rgba(16, 19, 34, 0.08);
--ring: 0 0 0 3px rgba(91, 91, 240, 0.28);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(120% 80% at 100% 0%, var(--brand-50), transparent 60%),
radial-gradient(120% 80% at 0% 100%, var(--accent-soft), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.shell {
min-height: 100vh;
display: grid;
place-items: center;
padding: 32px 16px;
}
/* ── Card ───────────────────────────────────────────── */
.card {
width: 100%;
max-width: 620px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-2);
padding: 28px 28px 22px;
}
.card__head {
margin-bottom: 22px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 9px;
font-weight: 700;
font-size: 0.86rem;
color: var(--ink-2);
margin-bottom: 14px;
}
.brand__mark {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 9px;
color: #fff;
background: linear-gradient(135deg, var(--brand), var(--brand-700));
box-shadow: var(--sh-1);
}
.card__head h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.card__sub {
margin: 6px 0 0;
color: var(--muted);
font-size: 0.92rem;
}
/* ── Progress steps ─────────────────────────────────── */
.steps {
margin-bottom: 22px;
}
.steps__list {
list-style: none;
display: flex;
justify-content: space-between;
gap: 6px;
margin: 0 0 12px;
padding: 0;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
gap: 7px;
flex: 1;
min-width: 0;
text-align: center;
}
.step__dot {
position: relative;
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 50%;
background: var(--white);
border: 2px solid var(--line-2);
color: var(--muted);
font-weight: 700;
font-size: 0.85rem;
transition: background 0.25s, border-color 0.25s, color 0.25s, box-shadow 0.25s;
}
.step__num {
transition: opacity 0.2s;
}
.step__check {
position: absolute;
opacity: 0;
transform: scale(0.6);
transition: opacity 0.2s, transform 0.2s;
}
.step__label {
font-size: 0.78rem;
font-weight: 600;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
transition: color 0.25s;
}
/* current step */
.step.is-current .step__dot {
background: var(--brand);
border-color: var(--brand);
color: #fff;
box-shadow: var(--ring);
}
.step.is-current .step__label {
color: var(--ink);
}
/* completed step */
.step.is-done .step__dot {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.step.is-done .step__num {
opacity: 0;
}
.step.is-done .step__check {
opacity: 1;
transform: scale(1);
}
.step.is-done .step__label {
color: var(--ink-2);
}
.steps__bar {
height: 6px;
border-radius: 999px;
background: var(--line);
overflow: hidden;
}
.steps__fill {
display: block;
height: 100%;
width: 25%;
border-radius: 999px;
background: linear-gradient(90deg, var(--brand), var(--accent));
transition: width 0.35s cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* ── Slide viewport ─────────────────────────────────── */
.viewport {
position: relative;
overflow: hidden;
}
.track {
position: relative;
}
.panel {
animation: slide-in 0.32s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
.panel.is-leaving-fwd {
animation: slide-out-left 0.28s ease both;
}
.panel.is-leaving-back {
animation: slide-out-right 0.28s ease both;
}
.panel.is-entering-back {
animation: slide-in-back 0.32s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
@keyframes slide-in {
from { opacity: 0; transform: translateX(24px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slide-in-back {
from { opacity: 0; transform: translateX(-24px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slide-out-left {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(-24px); }
}
@keyframes slide-out-right {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(24px); }
}
.panel__title {
margin: 0;
font-size: 1.2rem;
font-weight: 700;
letter-spacing: -0.01em;
outline: none;
}
.panel__hint {
margin: 4px 0 18px;
color: var(--muted);
font-size: 0.9rem;
}
/* ── Fields ─────────────────────────────────────────── */
.field {
margin-bottom: 16px;
}
.field > label,
.field--group > legend {
display: block;
font-size: 0.86rem;
font-weight: 600;
color: var(--ink-2);
margin-bottom: 6px;
padding: 0;
}
.req {
color: var(--danger);
font-weight: 700;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
input[type="text"],
input[type="email"],
input[type="password"] {
width: 100%;
font: inherit;
color: var(--ink);
padding: 11px 13px;
background: var(--white);
border: 1.5px solid var(--line-2);
border-radius: var(--r-sm);
box-shadow: var(--sh-1);
transition: border-color 0.18s, box-shadow 0.18s, background 0.18s;
}
input::placeholder {
color: #aab0c7;
}
input:hover:not(:disabled) {
border-color: rgba(16, 19, 34, 0.26);
}
input:focus-visible {
outline: none;
border-color: var(--brand);
box-shadow: var(--ring);
}
input:disabled {
background: var(--bg);
color: var(--muted);
cursor: not-allowed;
box-shadow: none;
}
.input-wrap {
position: relative;
}
.input-wrap input {
padding-right: 44px;
}
.reveal {
position: absolute;
top: 50%;
right: 6px;
transform: translateY(-50%);
display: grid;
place-items: center;
width: 32px;
height: 32px;
border: 0;
border-radius: var(--r-sm);
background: transparent;
color: var(--muted);
cursor: pointer;
transition: color 0.15s, background 0.15s;
}
.reveal:hover {
color: var(--ink-2);
background: var(--bg);
}
.reveal:focus-visible {
outline: none;
box-shadow: var(--ring);
color: var(--brand);
}
.help {
margin: 6px 0 0;
font-size: 0.79rem;
color: var(--muted);
display: flex;
align-items: flex-start;
gap: 5px;
}
/* error state */
.field.is-error input[type="text"],
.field.is-error input[type="email"],
.field.is-error input[type="password"] {
border-color: var(--danger);
box-shadow: 0 0 0 3px rgba(212, 80, 62, 0.18);
}
.field.is-error .help {
color: var(--danger);
font-weight: 600;
}
.field.is-error .chips,
.field.is-error .plans {
outline: 2px solid rgba(212, 80, 62, 0.4);
outline-offset: 4px;
border-radius: var(--r-sm);
}
/* success state */
.field.is-ok input[type="text"],
.field.is-ok input[type="email"],
.field.is-ok input[type="password"] {
border-color: var(--ok);
}
.field.is-ok .help {
color: var(--ok);
}
/* ── Chips (radio) ──────────────────────────────────── */
.field--group {
border: 0;
margin: 0 0 16px;
padding: 0;
min-width: 0;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 9px;
}
.chip {
display: inline-flex;
align-items: center;
padding: 9px 15px;
border: 1.5px solid var(--line-2);
border-radius: 999px;
font-size: 0.86rem;
font-weight: 600;
color: var(--ink-2);
background: var(--white);
cursor: pointer;
transition: border-color 0.16s, background 0.16s, color 0.16s, box-shadow 0.16s;
}
.chip input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.chip:hover {
border-color: var(--brand);
}
.chip:has(input:checked) {
border-color: var(--brand);
background: var(--brand-50);
color: var(--brand-700);
}
.chip:has(input:focus-visible) {
box-shadow: var(--ring);
}
/* ── Plans ──────────────────────────────────────────── */
.plans {
display: grid;
gap: 12px;
}
.plan {
position: relative;
display: block;
padding: 14px 16px 14px 46px;
border: 1.5px solid var(--line-2);
border-radius: var(--r-md);
background: var(--white);
cursor: pointer;
transition: border-color 0.16s, box-shadow 0.16s, background 0.16s;
}
.plan input {
position: absolute;
top: 17px;
left: 16px;
width: 18px;
height: 18px;
accent-color: var(--brand);
cursor: pointer;
}
.plan:hover {
border-color: var(--brand);
}
.plan:has(input:checked) {
border-color: var(--brand);
background: var(--brand-50);
box-shadow: 0 0 0 1px var(--brand) inset;
}
.plan:has(input:focus-visible) {
box-shadow: var(--ring);
}
.plan__body {
display: grid;
grid-template-columns: 1fr auto;
align-items: baseline;
gap: 2px 12px;
}
.plan__name {
font-weight: 700;
font-size: 0.98rem;
}
.plan__price {
font-weight: 800;
font-size: 1.05rem;
color: var(--ink);
}
.plan__price small {
font-size: 0.72rem;
font-weight: 600;
color: var(--muted);
}
.plan__desc {
grid-column: 1 / -1;
font-size: 0.82rem;
color: var(--muted);
}
.plan__badge {
position: absolute;
top: -9px;
right: 14px;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #fff;
background: var(--accent);
padding: 3px 9px;
border-radius: 999px;
}
/* ── Switch ─────────────────────────────────────────── */
.switch {
display: inline-flex;
align-items: center;
gap: 11px;
cursor: pointer;
font-size: 0.88rem;
font-weight: 600;
color: var(--ink-2);
margin-top: 4px;
}
.switch input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.switch__track {
position: relative;
width: 42px;
height: 24px;
border-radius: 999px;
background: var(--line-2);
transition: background 0.2s;
flex: none;
}
.switch__thumb {
position: absolute;
top: 3px;
left: 3px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
box-shadow: var(--sh-1);
transition: transform 0.2s;
}
.switch input:checked + .switch__track {
background: var(--brand);
}
.switch input:checked + .switch__track .switch__thumb {
transform: translateX(18px);
}
.switch input:focus-visible + .switch__track {
box-shadow: var(--ring);
}
.switch__text em {
color: var(--accent);
font-style: normal;
font-weight: 700;
}
/* ── Summary (review) ───────────────────────────────── */
.summary {
margin: 0 0 18px;
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
}
.summary__row {
display: grid;
grid-template-columns: 130px 1fr auto;
align-items: center;
gap: 10px;
padding: 11px 14px;
border-bottom: 1px solid var(--line);
}
.summary__row:last-child {
border-bottom: 0;
}
.summary__row:nth-child(odd) {
background: #fafbff;
}
.summary__key {
font-size: 0.8rem;
font-weight: 600;
color: var(--muted);
}
.summary__val {
font-size: 0.9rem;
font-weight: 600;
color: var(--ink);
word-break: break-word;
}
.summary__edit {
border: 0;
background: transparent;
color: var(--brand);
font: inherit;
font-size: 0.8rem;
font-weight: 700;
cursor: pointer;
padding: 3px 6px;
border-radius: 6px;
}
.summary__edit:hover {
background: var(--brand-50);
}
.summary__edit:focus-visible {
outline: none;
box-shadow: var(--ring);
}
/* ── Checkbox (terms) ───────────────────────────────── */
.check {
display: flex;
align-items: flex-start;
gap: 10px;
cursor: pointer;
font-size: 0.88rem;
color: var(--ink-2);
}
.check input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.check__box {
flex: none;
display: grid;
place-items: center;
width: 20px;
height: 20px;
margin-top: 1px;
border: 1.5px solid var(--line-2);
border-radius: 6px;
background: var(--white);
color: #fff;
transition: background 0.15s, border-color 0.15s;
}
.check__box svg {
opacity: 0;
transform: scale(0.6);
transition: opacity 0.15s, transform 0.15s;
}
.check input:checked + .check__box {
background: var(--brand);
border-color: var(--brand);
}
.check input:checked + .check__box svg {
opacity: 1;
transform: scale(1);
}
.check input:focus-visible + .check__box {
box-shadow: var(--ring);
}
.field.is-error + .check .check__box,
.check.is-error .check__box {
border-color: var(--danger);
}
/* ── Success screen ─────────────────────────────────── */
.panel--done {
text-align: center;
padding: 18px 8px 8px;
}
.done__mark {
display: grid;
place-items: center;
width: 76px;
height: 76px;
margin: 0 auto 16px;
border-radius: 50%;
color: #fff;
background: linear-gradient(135deg, var(--accent), var(--ok));
box-shadow: 0 10px 26px rgba(47, 158, 111, 0.32);
animation: pop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes pop {
0% { transform: scale(0.4); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
.done__text {
margin: 8px auto 20px;
max-width: 360px;
color: var(--muted);
font-size: 0.92rem;
}
/* ── Alert ──────────────────────────────────────────── */
.form__alert {
display: flex;
align-items: center;
gap: 8px;
margin-top: 16px;
padding: 10px 13px;
border-radius: var(--r-sm);
background: rgba(212, 80, 62, 0.08);
border: 1px solid rgba(212, 80, 62, 0.3);
color: var(--danger);
font-size: 0.84rem;
font-weight: 600;
}
/* ── Actions ────────────────────────────────────────── */
.actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 22px;
padding-top: 20px;
border-top: 1px solid var(--line);
}
.actions__count {
font-size: 0.8rem;
font-weight: 600;
color: var(--muted);
}
.btn {
display: inline-flex;
align-items: center;
gap: 7px;
font: inherit;
font-size: 0.9rem;
font-weight: 700;
padding: 11px 18px;
border-radius: var(--r-sm);
border: 1.5px solid transparent;
cursor: pointer;
transition: background 0.16s, color 0.16s, border-color 0.16s, box-shadow 0.16s, transform 0.06s;
}
.btn:active {
transform: translateY(1px);
}
.btn:focus-visible {
outline: none;
box-shadow: var(--ring);
}
.btn--primary {
background: var(--brand);
color: #fff;
}
.btn--primary:hover {
background: var(--brand-d);
}
.btn--ghost {
background: var(--white);
color: var(--ink-2);
border-color: var(--line-2);
}
.btn--ghost:hover:not(:disabled) {
border-color: var(--brand);
color: var(--brand-700);
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn.is-busy {
pointer-events: none;
}
/* ── Toast ──────────────────────────────────────────── */
.toast-region {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 50;
width: min(340px, calc(100% - 32px));
}
.toast {
display: flex;
align-items: center;
gap: 9px;
padding: 11px 14px;
border-radius: var(--r-md);
background: var(--ink);
color: #fff;
font-size: 0.85rem;
font-weight: 600;
box-shadow: var(--sh-2);
animation: toast-in 0.25s ease both;
}
.toast--ok {
background: var(--ok);
}
.toast--warn {
background: var(--warn);
}
.toast.is-out {
animation: toast-out 0.25s ease forwards;
}
@keyframes toast-in {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toast-out {
to { opacity: 0; transform: translateY(12px); }
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ── Responsive ─────────────────────────────────────── */
@media (max-width: 520px) {
.shell {
padding: 16px 12px;
}
.card {
padding: 20px 16px 16px;
border-radius: var(--r-md);
}
.card__head h1 {
font-size: 1.3rem;
}
.grid-2 {
grid-template-columns: 1fr;
gap: 0;
}
.step__label {
font-size: 0;
}
.step__label::first-letter {
font-size: 0.78rem;
}
.step__dot {
width: 30px;
height: 30px;
}
.summary__row {
grid-template-columns: 1fr auto;
}
.summary__key {
grid-column: 1 / -1;
}
.actions {
flex-wrap: wrap;
}
.actions__count {
order: 3;
width: 100%;
text-align: center;
}
.btn {
flex: 1;
justify-content: center;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}(function () {
"use strict";
var form = document.getElementById("wizard");
if (!form) return;
var track = document.getElementById("track");
var panels = Array.prototype.slice.call(track.querySelectorAll(".panel"));
var stepItems = Array.prototype.slice.call(document.querySelectorAll("#stepNav .step"));
var stepFill = document.getElementById("stepFill");
var stepCount = document.getElementById("stepCount");
var backBtn = document.getElementById("backBtn");
var nextBtn = document.getElementById("nextBtn");
var actions = document.getElementById("actions");
var formAlert = document.getElementById("formAlert");
var summary = document.getElementById("summary");
var toaster = document.getElementById("toaster");
var TOTAL = 4; // input steps; index 4 is the success screen
var current = 0;
// ── Toast helper ──────────────────────────────────
function toast(msg, kind) {
var el = document.createElement("div");
el.className = "toast" + (kind ? " toast--" + kind : "");
var icon =
kind === "ok"
? '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg>'
: kind === "warn"
? '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4m0 4h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>'
: "";
el.innerHTML = icon + "<span>" + msg + "</span>";
toaster.appendChild(el);
setTimeout(function () {
el.classList.add("is-out");
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 260);
}, 2600);
}
// ── Validators per step ───────────────────────────
function setError(input, msg) {
var field = input.closest(".field");
if (!field) return;
field.classList.remove("is-ok");
field.classList.add("is-error");
input.setAttribute("aria-invalid", "true");
var help = field.querySelector(".help");
if (help) {
if (help.dataset.base === undefined) help.dataset.base = help.textContent;
help.textContent = msg;
}
}
function clearError(input) {
var field = input.closest(".field");
if (!field) return;
field.classList.remove("is-error");
input.removeAttribute("aria-invalid");
var help = field.querySelector(".help");
if (help && help.dataset.base !== undefined) {
help.textContent = help.dataset.base;
}
}
function setOk(input) {
clearError(input);
var field = input.closest(".field");
if (field) field.classList.add("is-ok");
}
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
// Each validator returns an array of {input, msg} for the first failing fields.
function validateStep(index, report) {
var errors = [];
var fail = function (input, msg) {
if (report) setError(input, msg);
errors.push({ input: input, msg: msg });
};
var pass = function (input) {
if (report) setOk(input);
};
if (index === 0) {
var email = form.email;
var pw = form.password;
var confirm = form.confirm;
if (!email.value.trim()) fail(email, "Email is required.");
else if (!EMAIL_RE.test(email.value.trim())) fail(email, "Enter a valid email address.");
else pass(email);
if (!pw.value) fail(pw, "Password is required.");
else if (pw.value.length < 8) fail(pw, "Use at least 8 characters.");
else if (!/\d/.test(pw.value)) fail(pw, "Include at least one number.");
else pass(pw);
if (!confirm.value) fail(confirm, "Please confirm your password.");
else if (confirm.value !== pw.value) fail(confirm, "Passwords don't match.");
else if (pw.value.length >= 8) pass(confirm);
} else if (index === 1) {
var fn = form.firstName;
var ln = form.lastName;
var co = form.company;
if (!fn.value.trim()) fail(fn, "First name is required."); else pass(fn);
if (!ln.value.trim()) fail(ln, "Last name is required."); else pass(ln);
if (!co.value.trim()) fail(co, "Company is required."); else pass(co);
var size = form.querySelector('input[name="teamSize"]:checked');
if (!size) {
var sizeField = document.getElementById("sizeGroup").closest(".field");
if (report && sizeField) sizeField.classList.add("is-error");
errors.push({ input: document.getElementById("sizeGroup"), msg: "Pick a team size." });
} else {
var sf = document.getElementById("sizeGroup").closest(".field");
if (sf) sf.classList.remove("is-error");
}
} else if (index === 2) {
var plan = form.querySelector('input[name="plan"]:checked');
var planField = document.getElementById("planGroup").closest(".field");
if (!plan) {
if (report && planField) planField.classList.add("is-error");
errors.push({ input: document.getElementById("planGroup"), msg: "Choose a plan." });
} else if (planField) {
planField.classList.remove("is-error");
}
} else if (index === 3) {
var terms = form.terms;
var termsLabel = terms.closest(".check");
if (!terms.checked) {
if (report && termsLabel) termsLabel.classList.add("is-error");
errors.push({ input: terms, msg: "You must accept the terms to continue." });
} else if (termsLabel) {
termsLabel.classList.remove("is-error");
}
}
return errors;
}
function isStepValid(index) {
return validateStep(index, false).length === 0;
}
// ── Progress / chrome sync ────────────────────────
function refreshNextState() {
if (current >= TOTAL) return;
nextBtn.disabled = !isStepValid(current);
}
function updateChrome() {
var pct = ((current + 1) / TOTAL) * 100;
stepFill.style.width = Math.min(pct, 100) + "%";
stepItems.forEach(function (item, i) {
item.classList.toggle("is-current", i === current);
item.classList.toggle("is-done", i < current);
if (i === current) item.setAttribute("aria-current", "step");
else item.removeAttribute("aria-current");
});
if (current >= TOTAL) {
actions.style.display = "none";
return;
}
actions.style.display = "";
backBtn.disabled = current === 0;
stepCount.textContent = "Step " + (current + 1) + " of " + TOTAL;
var last = current === TOTAL - 1;
nextBtn.firstChild && (nextBtn.childNodes[0].nodeValue = last ? "Create workspace" : "Next");
var svg = nextBtn.querySelector("svg");
if (svg) svg.style.display = last ? "none" : "";
refreshNextState();
}
function focusHeading(index) {
var panel = panels[index];
var h = panel && panel.querySelector(".panel__title");
if (h) h.focus();
}
// ── Step transition with slide ────────────────────
function goTo(index, dir) {
if (index === current) return;
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var leaving = panels[current];
var entering = panels[index];
formAlert.hidden = true;
if (reduce) {
leaving.hidden = true;
entering.hidden = false;
current = index;
updateChrome();
focusHeading(index);
return;
}
leaving.classList.add(dir === "back" ? "is-leaving-back" : "is-leaving-fwd");
leaving.addEventListener(
"animationend",
function handler() {
leaving.removeEventListener("animationend", handler);
leaving.classList.remove("is-leaving-back", "is-leaving-fwd");
leaving.hidden = true;
entering.hidden = false;
entering.classList.remove("is-entering-back");
if (dir === "back") {
// restart the back-entry animation
entering.classList.add("is-entering-back");
void entering.offsetWidth;
} else {
// re-trigger forward slide-in
entering.style.animation = "none";
void entering.offsetWidth;
entering.style.animation = "";
}
current = index;
updateChrome();
focusHeading(index);
},
{ once: false }
);
}
// ── Review summary builder ────────────────────────
function val(name, fallback) {
var el = form[name];
if (!el) return fallback || "—";
if (el.value !== undefined && el.type !== "radio") return el.value.trim() || fallback || "—";
var checked = form.querySelector('input[name="' + name + '"]:checked');
return checked ? checked.value : fallback || "—";
}
function buildSummary() {
var billing = form.annual.checked ? "Annual (−20%)" : "Monthly";
var rows = [
{ k: "Email", v: val("email"), step: 0 },
{ k: "Name", v: (val("firstName") + " " + val("lastName")).trim() || "—", step: 1 },
{ k: "Company", v: val("company"), step: 1 },
{ k: "Team size", v: val("teamSize"), step: 1 },
{ k: "Plan", v: val("plan", "—"), step: 2 },
{ k: "Billing", v: billing, step: 2 },
];
summary.innerHTML = "";
rows.forEach(function (r) {
var row = document.createElement("div");
row.className = "summary__row";
var dt = document.createElement("dt");
dt.className = "summary__key";
dt.textContent = r.k;
var dd = document.createElement("dd");
dd.className = "summary__val";
dd.style.margin = "0";
dd.textContent = r.v;
var btn = document.createElement("button");
btn.type = "button";
btn.className = "summary__edit";
btn.textContent = "Edit";
btn.setAttribute("aria-label", "Edit " + r.k);
btn.addEventListener("click", function () {
goTo(r.step, "back");
});
row.appendChild(dt);
row.appendChild(dd);
row.appendChild(btn);
summary.appendChild(row);
});
}
// ── Next / Back handlers ──────────────────────────
function handleNext() {
var errors = validateStep(current, true);
if (errors.length) {
formAlert.hidden = false;
formAlert.textContent =
errors.length === 1
? errors[0].msg
: "Please fix " + errors.length + " fields before continuing.";
var first = errors[0].input;
if (first && first.focus) first.focus();
toast("Some fields need attention.", "warn");
return;
}
if (current === TOTAL - 1) {
submit();
return;
}
buildSummaryIfNeeded(current + 1);
goTo(current + 1, "fwd");
}
function buildSummaryIfNeeded(nextIndex) {
if (nextIndex === 3) buildSummary();
}
function handleBack() {
if (current === 0) return;
goTo(current - 1, "back");
}
// ── Submit → success ──────────────────────────────
function submit() {
nextBtn.classList.add("is-busy");
nextBtn.disabled = true;
nextBtn.childNodes[0].nodeValue = "Creating…";
// Simulated async create (no network) so the busy state is visible.
setTimeout(function () {
var done = panels[4];
var doneText = document.getElementById("doneText");
if (doneText) {
doneText.textContent =
"You're all set, " +
(val("firstName") || "there") +
". We've sent a confirmation to " +
val("email") +
".";
}
panels[current].hidden = true;
done.hidden = false;
current = 4;
stepItems.forEach(function (it) {
it.classList.remove("is-current");
it.classList.add("is-done");
it.removeAttribute("aria-current");
});
stepFill.style.width = "100%";
updateChrome();
focusHeading(4);
toast("Workspace created.", "ok");
nextBtn.classList.remove("is-busy");
}, 700);
}
// ── Restart ───────────────────────────────────────
document.getElementById("restart").addEventListener("click", function () {
form.reset();
panels.forEach(function (p, i) {
p.hidden = i !== 0;
});
form.querySelectorAll(".field").forEach(function (f) {
f.classList.remove("is-error", "is-ok");
});
form.querySelectorAll(".check").forEach(function (c) {
c.classList.remove("is-error");
});
formAlert.hidden = true;
current = 0;
updateChrome();
focusHeading(0);
});
// ── Password reveal toggle ────────────────────────
form.querySelectorAll("[data-toggle]").forEach(function (btn) {
btn.addEventListener("click", function () {
var input = document.getElementById(btn.dataset.toggle);
var show = input.type === "password";
input.type = show ? "text" : "password";
btn.setAttribute("aria-pressed", String(show));
btn.setAttribute("aria-label", show ? "Hide password" : "Show password");
});
});
// ── Live re-validate as the user types/selects ────
form.addEventListener("input", function (e) {
var t = e.target;
if (t && t.closest(".field") && t.closest(".field").classList.contains("is-error")) {
// soft clear on edit; full check on blur / next
validateStep(current, true);
}
refreshNextState();
});
form.addEventListener("change", function () {
// radios, checkboxes, switch
if (current === 3) validateStep(3, true);
refreshNextState();
});
form.addEventListener(
"blur",
function (e) {
var t = e.target;
if (t && t.tagName === "INPUT" && t.closest(".field")) {
validateStep(current, true);
refreshNextState();
}
},
true
);
nextBtn.addEventListener("click", handleNext);
backBtn.addEventListener("click", handleBack);
// Enter advances instead of submitting the whole form.
form.addEventListener("submit", function (e) {
e.preventDefault();
handleNext();
});
form.addEventListener("keydown", function (e) {
if (e.key === "Enter" && e.target.tagName === "INPUT") {
e.preventDefault();
handleNext();
}
});
// ── Init ──────────────────────────────────────────
updateChrome();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Multi-step signup — progress + back/next</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell">
<section class="card" aria-labelledby="wizard-title">
<header class="card__head">
<div class="brand">
<span class="brand__mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2 4 6v6c0 5 3.4 8.5 8 10 4.6-1.5 8-5 8-10V6l-8-4z" />
<path d="m9 12 2 2 4-4" />
</svg>
</span>
<span class="brand__name">Northwind Cloud</span>
</div>
<h1 id="wizard-title">Create your workspace</h1>
<p class="card__sub">Four quick steps. Your progress is kept as you move back and forth.</p>
</header>
<!-- Progress indicator -->
<nav class="steps" aria-label="Signup progress">
<ol class="steps__list" id="stepNav">
<li class="step" data-step="0">
<span class="step__dot"><span class="step__num">1</span>
<svg class="step__check" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5" /></svg>
</span>
<span class="step__label">Account</span>
</li>
<li class="step" data-step="1">
<span class="step__dot"><span class="step__num">2</span>
<svg class="step__check" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5" /></svg>
</span>
<span class="step__label">Profile</span>
</li>
<li class="step" data-step="2">
<span class="step__dot"><span class="step__num">3</span>
<svg class="step__check" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5" /></svg>
</span>
<span class="step__label">Plan</span>
</li>
<li class="step" data-step="3">
<span class="step__dot"><span class="step__num">4</span>
<svg class="step__check" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5" /></svg>
</span>
<span class="step__label">Review</span>
</li>
</ol>
<div class="steps__bar" aria-hidden="true"><span class="steps__fill" id="stepFill"></span></div>
</nav>
<form id="wizard" class="form" novalidate>
<div class="viewport">
<div class="track" id="track">
<!-- STEP 1: ACCOUNT -->
<section class="panel" data-panel="0" role="group" aria-labelledby="h-account">
<h2 class="panel__title" id="h-account" tabindex="-1">Account</h2>
<p class="panel__hint">This is how you'll sign in.</p>
<div class="field">
<label for="email">Work email <span class="req" aria-hidden="true">*</span></label>
<input id="email" name="email" type="email" inputmode="email" autocomplete="email"
placeholder="ada@northwind.io" aria-describedby="email-help" required />
<p class="help" id="email-help">We'll send a verification link here.</p>
</div>
<div class="grid-2">
<div class="field">
<label for="password">Password <span class="req" aria-hidden="true">*</span></label>
<div class="input-wrap">
<input id="password" name="password" type="password" autocomplete="new-password"
placeholder="At least 8 characters" aria-describedby="password-help" required />
<button type="button" class="reveal" data-toggle="password" aria-label="Show password" aria-pressed="false">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" /><circle cx="12" cy="12" r="3" /></svg>
</button>
</div>
<p class="help" id="password-help">Use 8+ characters with a number.</p>
</div>
<div class="field">
<label for="confirm">Confirm password <span class="req" aria-hidden="true">*</span></label>
<input id="confirm" name="confirm" type="password" autocomplete="new-password"
placeholder="Re-enter password" aria-describedby="confirm-help" required />
<p class="help" id="confirm-help">Must match the password above.</p>
</div>
</div>
</section>
<!-- STEP 2: PROFILE -->
<section class="panel" data-panel="1" role="group" aria-labelledby="h-profile" hidden>
<h2 class="panel__title" id="h-profile" tabindex="-1">Profile</h2>
<p class="panel__hint">Tell us a little about you.</p>
<div class="grid-2">
<div class="field">
<label for="firstName">First name <span class="req" aria-hidden="true">*</span></label>
<input id="firstName" name="firstName" type="text" autocomplete="given-name"
placeholder="Ada" aria-describedby="firstName-help" required />
<p class="help" id="firstName-help">As you'd like it shown to teammates.</p>
</div>
<div class="field">
<label for="lastName">Last name <span class="req" aria-hidden="true">*</span></label>
<input id="lastName" name="lastName" type="text" autocomplete="family-name"
placeholder="Lovelace" aria-describedby="lastName-help" required />
<p class="help" id="lastName-help">Your surname.</p>
</div>
</div>
<div class="field">
<label for="company">Company <span class="req" aria-hidden="true">*</span></label>
<input id="company" name="company" type="text" autocomplete="organization"
placeholder="Northwind Labs" aria-describedby="company-help" required />
<p class="help" id="company-help">The organization this workspace belongs to.</p>
</div>
<fieldset class="field field--group">
<legend>Team size <span class="req" aria-hidden="true">*</span></legend>
<div class="chips" role="radiogroup" aria-describedby="size-help" id="sizeGroup">
<label class="chip"><input type="radio" name="teamSize" value="1–5" />Just me (1–5)</label>
<label class="chip"><input type="radio" name="teamSize" value="6–25" />6–25</label>
<label class="chip"><input type="radio" name="teamSize" value="26–100" />26–100</label>
<label class="chip"><input type="radio" name="teamSize" value="100+" />100+</label>
</div>
<p class="help" id="size-help">Pick the range that best fits your team.</p>
</fieldset>
</section>
<!-- STEP 3: PLAN -->
<section class="panel" data-panel="2" role="group" aria-labelledby="h-plan" hidden>
<h2 class="panel__title" id="h-plan" tabindex="-1">Plan</h2>
<p class="panel__hint">Choose a plan — change it anytime.</p>
<fieldset class="field field--group">
<legend class="sr-only">Subscription plan</legend>
<div class="plans" role="radiogroup" aria-describedby="plan-help" id="planGroup">
<label class="plan">
<input type="radio" name="plan" value="Starter" />
<span class="plan__body">
<span class="plan__name">Starter</span>
<span class="plan__price">$0<small>/mo</small></span>
<span class="plan__desc">Up to 3 projects, community support.</span>
</span>
</label>
<label class="plan">
<input type="radio" name="plan" value="Team" />
<span class="plan__badge">Popular</span>
<span class="plan__body">
<span class="plan__name">Team</span>
<span class="plan__price">$24<small>/mo</small></span>
<span class="plan__desc">Unlimited projects, priority support.</span>
</span>
</label>
<label class="plan">
<input type="radio" name="plan" value="Scale" />
<span class="plan__body">
<span class="plan__name">Scale</span>
<span class="plan__price">$60<small>/mo</small></span>
<span class="plan__desc">SSO, audit logs, dedicated manager.</span>
</span>
</label>
</div>
<p class="help" id="plan-help">All plans include a 14-day trial.</p>
</fieldset>
<label class="switch">
<input type="checkbox" name="annual" id="annual" />
<span class="switch__track" aria-hidden="true"><span class="switch__thumb"></span></span>
<span class="switch__text">Bill annually <em>— save 20%</em></span>
</label>
</section>
<!-- STEP 4: REVIEW -->
<section class="panel" data-panel="3" role="group" aria-labelledby="h-review" hidden>
<h2 class="panel__title" id="h-review" tabindex="-1">Review</h2>
<p class="panel__hint">Check everything looks right before you finish.</p>
<dl class="summary" id="summary"></dl>
<label class="check">
<input type="checkbox" name="terms" id="terms" aria-describedby="terms-help" required />
<span class="check__box" aria-hidden="true">
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m20 6-11 11-5-5" /></svg>
</span>
<span class="check__text">I agree to the Terms of Service and Privacy Policy. <span class="req" aria-hidden="true">*</span></span>
</label>
<p class="help" id="terms-help">Required to create your workspace.</p>
</section>
<!-- SUCCESS -->
<section class="panel panel--done" data-panel="4" role="group" aria-labelledby="h-done" hidden>
<div class="done">
<span class="done__mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m20 6-11 11-5-5" /></svg>
</span>
<h2 class="panel__title" id="h-done" tabindex="-1">Workspace created</h2>
<p class="done__text" id="doneText">You're all set. We've sent a confirmation to your inbox.</p>
<button type="button" class="btn btn--primary" id="restart">Start over</button>
</div>
</section>
</div>
</div>
<!-- Error summary (announced) -->
<div class="form__alert" id="formAlert" role="alert" hidden></div>
<footer class="actions" id="actions">
<button type="button" class="btn btn--ghost" id="backBtn" disabled>
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6" /></svg>
Back
</button>
<span class="actions__count" id="stepCount" aria-live="polite">Step 1 of 4</span>
<button type="button" class="btn btn--primary" id="nextBtn">
Next
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18l6-6-6-6" /></svg>
</button>
</footer>
</form>
</section>
<div class="toast-region" id="toaster" aria-live="polite" aria-atomic="true"></div>
</main>
<script src="script.js"></script>
</body>
</html>Multi-step (progress + back/next)
A self-contained signup wizard that splits a long form into four manageable steps — Account, Profile, Plan, and Review. A numbered step rail at the top marks the current step with aria-current, fills completed steps with a checkmark, and an animated gradient bar tracks overall progress. The Next button stays disabled until the active step’s fields are valid, so users never advance past an incomplete section.
Validation is real and per-step: email format, password length and digit checks, a matching confirm field, required name/company text, a chosen team size and plan, and accepted terms. Errors surface inline with aria-invalid, helper text turns into the error message, and an role="alert" summary announces how many fields need attention. Moving forward slides the next panel in; Back slides it the other way and keeps every entered value intact. Focus jumps to the new step’s heading on each change, and prefers-reduced-motion collapses the transitions.
The final Review step builds a live summary of every answer with an Edit button per row that jumps straight back to the relevant step. Submitting shows a brief busy state, then a confirmation screen personalized with the user’s name and email — plus a Start over button to reset the whole flow. A small toast() helper provides non-blocking feedback. No frameworks, no build step, no external assets.