Banking — Account-Open Wizard
A trust-first fintech account-opening (KYC) wizard that walks new customers through four guided steps — personal details, home address, identity verification and a final review. It features a vertical progress stepper, an animated progress bar, inline field validation with friendly errors, an age and email check, a mock ID and selfie upload with scanning progress, an editable summary, terms acceptance and an encrypted success screen with a generated application reference and masked IBAN. Built with semantic HTML, Inter typography and responsive, accessible vanilla JavaScript.
MCP
Code
:root {
--navy: #0e1b3a;
--navy-2: #16264d;
--ink: #0e1726;
--ink-2: #3a4660;
--muted: #697089;
--accent: #3b6ef6;
--accent-d: #2a55cc;
--accent-50: #eaf0ff;
--teal: #0fb5a6;
--violet: #7c5cff;
--bg: #f5f7fb;
--surface: #ffffff;
--line: rgba(14, 27, 58, 0.1);
--line-2: rgba(14, 27, 58, 0.18);
--ok: #1f9d62;
--warn: #d9982b;
--danger: #d4493e;
--credit: #1f9d62;
--debit: #0e1726;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-sm: 0 1px 2px rgba(14, 27, 58, 0.06), 0 1px 3px rgba(14, 27, 58, 0.08);
--sh-md: 0 6px 18px rgba(14, 27, 58, 0.08), 0 2px 6px rgba(14, 27, 58, 0.06);
--sh-lg: 0 24px 60px rgba(14, 27, 58, 0.16);
}
* {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
min-height: 100vh;
}
.mono {
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
}
/* ---------- Shell ---------- */
.shell {
display: grid;
grid-template-columns: 320px 1fr;
max-width: 1040px;
margin: 0 auto;
min-height: 100vh;
}
/* ---------- Brand rail ---------- */
.rail {
background: linear-gradient(165deg, var(--navy) 0%, var(--navy-2) 70%, #1d3160 100%);
color: #fff;
padding: 34px 30px;
display: flex;
flex-direction: column;
gap: 26px;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 800;
font-size: 20px;
letter-spacing: -0.01em;
}
.brand__mark {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 9px;
background: linear-gradient(140deg, var(--accent), var(--violet));
font-size: 14px;
box-shadow: 0 4px 12px rgba(59, 110, 246, 0.5);
}
.rail__lede {
margin: 0;
color: rgba(255, 255, 255, 0.72);
font-size: 14px;
}
/* Stepper */
.stepper {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
position: relative;
}
.stepper__item {
display: flex;
align-items: center;
gap: 13px;
padding: 10px 4px;
position: relative;
opacity: 0.55;
transition: opacity 0.25s;
}
.stepper__item::before {
content: "";
position: absolute;
left: 17px;
top: 34px;
bottom: -10px;
width: 2px;
background: rgba(255, 255, 255, 0.16);
}
.stepper__item:last-child::before {
display: none;
}
.stepper__dot {
flex: none;
width: 36px;
height: 36px;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 700;
font-size: 14px;
background: rgba(255, 255, 255, 0.08);
border: 1.5px solid rgba(255, 255, 255, 0.22);
transition: all 0.25s;
z-index: 1;
}
.stepper__txt {
display: flex;
flex-direction: column;
}
.stepper__title {
font-weight: 600;
font-size: 14px;
}
.stepper__sub {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
.stepper__item.is-active {
opacity: 1;
}
.stepper__item.is-active .stepper__dot {
background: var(--accent);
border-color: var(--accent);
box-shadow: 0 0 0 4px rgba(59, 110, 246, 0.25);
}
.stepper__item.is-done {
opacity: 1;
}
.stepper__item.is-done .stepper__dot {
background: var(--teal);
border-color: var(--teal);
color: #fff;
font-size: 0;
}
.stepper__item.is-done .stepper__dot::after {
content: "✓";
font-size: 16px;
}
.stepper__item.is-done::before {
background: var(--teal);
}
.rail__secure {
margin-top: auto;
display: flex;
align-items: center;
gap: 9px;
font-size: 12.5px;
color: rgba(255, 255, 255, 0.62);
padding-top: 18px;
border-top: 1px solid rgba(255, 255, 255, 0.12);
}
/* ---------- Panel ---------- */
.panel {
background: var(--surface);
padding: 30px 44px 36px;
display: flex;
flex-direction: column;
}
.panel__head {
margin-bottom: 26px;
}
.panel__progress {
height: 6px;
border-radius: 99px;
background: var(--accent-50);
overflow: hidden;
}
.panel__progress-bar {
height: 100%;
border-radius: 99px;
background: linear-gradient(90deg, var(--accent), var(--teal));
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.panel__count {
margin: 9px 0 0;
font-size: 12.5px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* ---------- Steps ---------- */
.step {
display: none;
animation: rise 0.35s ease both;
}
.step.is-active {
display: block;
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: none;
}
}
.step__h {
margin: 0 0 6px;
font-size: 24px;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--navy);
}
.step__h span {
color: var(--accent);
}
.step__lede {
margin: 0 0 24px;
color: var(--muted);
font-size: 14.5px;
}
/* ---------- Fields ---------- */
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.field--full {
grid-column: 1 / -1;
}
.field__label {
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
}
.field input,
.field select {
font: inherit;
font-size: 14.5px;
color: var(--ink);
padding: 11px 13px;
border: 1.5px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--surface);
outline: none;
transition: border-color 0.18s, box-shadow 0.18s;
width: 100%;
}
.field select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23697089' d='M1 1l5 5 5-5'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 13px center;
padding-right: 34px;
}
.field input::placeholder {
color: #aab0c0;
}
.field input:focus,
.field select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 4px var(--accent-50);
}
.field.is-invalid input,
.field.is-invalid select {
border-color: var(--danger);
}
.field.is-invalid input:focus,
.field.is-invalid select:focus {
box-shadow: 0 0 0 4px rgba(212, 73, 62, 0.16);
}
.field__err {
font-size: 12.5px;
font-weight: 500;
color: var(--danger);
min-height: 0;
}
.field.is-invalid .field__err {
min-height: 16px;
}
/* ---------- Uploads ---------- */
.uploads {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 4px;
}
.upload {
display: flex;
align-items: center;
gap: 14px;
width: 100%;
text-align: left;
font: inherit;
cursor: pointer;
padding: 16px;
border: 1.5px dashed var(--line-2);
border-radius: var(--r-md);
background: var(--bg);
transition: border-color 0.18s, background 0.18s, transform 0.1s;
}
.upload:hover {
border-color: var(--accent);
background: var(--accent-50);
}
.upload:active {
transform: scale(0.995);
}
.upload__icon {
flex: none;
width: 44px;
height: 44px;
display: grid;
place-items: center;
font-size: 22px;
border-radius: 11px;
background: var(--surface);
box-shadow: var(--sh-sm);
}
.upload__txt {
display: flex;
flex-direction: column;
min-width: 0;
}
.upload__title {
font-weight: 600;
font-size: 14.5px;
color: var(--ink);
}
.upload__sub {
font-size: 12.5px;
color: var(--muted);
}
.upload__state {
margin-left: auto;
flex: none;
font-size: 12.5px;
font-weight: 700;
}
.upload.is-uploading {
border-style: solid;
border-color: var(--warn);
}
.upload.is-uploading .upload__state {
color: var(--warn);
}
.upload.is-done {
border-style: solid;
border-color: var(--ok);
background: rgba(31, 157, 98, 0.06);
}
.upload.is-done .upload__state {
color: var(--ok);
}
.note {
display: flex;
align-items: center;
gap: 8px;
margin: 18px 0 0;
font-size: 13px;
color: var(--muted);
}
/* ---------- Review ---------- */
.review {
display: flex;
flex-direction: column;
gap: 18px;
margin-bottom: 22px;
}
.review__group {
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
}
.review__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--bg);
border-bottom: 1px solid var(--line);
}
.review__head h2 {
margin: 0;
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--ink-2);
}
.review__edit {
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--accent);
background: none;
border: none;
cursor: pointer;
padding: 2px 4px;
border-radius: 6px;
}
.review__edit:hover {
text-decoration: underline;
}
.review__row {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 10px 16px;
font-size: 14px;
}
.review__row + .review__row {
border-top: 1px solid var(--line);
}
.review__k {
color: var(--muted);
}
.review__v {
font-weight: 600;
text-align: right;
word-break: break-word;
}
/* ---------- Checkbox ---------- */
.check {
display: flex;
align-items: flex-start;
gap: 11px;
font-size: 13.5px;
color: var(--ink-2);
cursor: pointer;
}
.check input {
flex: none;
width: 18px;
height: 18px;
margin-top: 1px;
accent-color: var(--accent);
cursor: pointer;
}
.check a {
color: var(--accent);
font-weight: 600;
text-decoration: none;
}
.check a:hover {
text-decoration: underline;
}
/* ---------- Pills ---------- */
.pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 10px;
border-radius: 99px;
font-size: 12px;
font-weight: 700;
}
.pill--pending {
background: rgba(217, 152, 43, 0.14);
color: var(--warn);
}
.pill--pending::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--warn);
}
/* ---------- Success ---------- */
.success {
text-align: center;
}
.success__badge {
width: 66px;
height: 66px;
margin: 6px auto 18px;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 32px;
color: #fff;
background: linear-gradient(140deg, var(--teal), var(--ok));
box-shadow: 0 12px 30px rgba(15, 181, 166, 0.4);
animation: pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes pop {
from {
transform: scale(0);
}
}
.success .step__lede {
max-width: 360px;
margin-inline: auto;
}
.success__card {
text-align: left;
border: 1px solid var(--line);
border-radius: var(--r-md);
max-width: 420px;
margin: 6px auto 0;
box-shadow: var(--sh-sm);
}
.success__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 13px 16px;
font-size: 14px;
}
.success__row + .success__row {
border-top: 1px solid var(--line);
}
.success__k {
color: var(--muted);
}
.success__v {
font-weight: 700;
}
.success__note {
justify-content: center;
}
/* ---------- Actions ---------- */
.actions {
display: flex;
gap: 12px;
margin-top: auto;
padding-top: 28px;
}
.btn {
font: inherit;
font-weight: 600;
font-size: 14.5px;
padding: 12px 22px;
border-radius: var(--r-sm);
border: 1.5px solid transparent;
cursor: pointer;
transition: transform 0.1s, box-shadow 0.18s, background 0.18s;
}
.btn:active {
transform: translateY(1px);
}
.btn--primary {
margin-left: auto;
background: var(--accent);
color: #fff;
box-shadow: 0 6px 16px rgba(59, 110, 246, 0.32);
}
.btn--primary:hover {
background: var(--accent-d);
}
.btn--primary:disabled {
opacity: 0.6;
cursor: progress;
}
.btn--ghost {
background: var(--surface);
color: var(--ink-2);
border-color: var(--line-2);
}
.btn--ghost:hover {
background: var(--bg);
}
/* ---------- Toaster ---------- */
.toaster {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 10px;
z-index: 50;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
background: var(--navy);
color: #fff;
font-size: 13.5px;
font-weight: 500;
padding: 12px 16px;
border-radius: var(--r-md);
box-shadow: var(--sh-lg);
animation: toastIn 0.3s ease both;
}
.toast--ok {
background: var(--ok);
}
.toast--err {
background: var(--danger);
}
.toast.is-out {
animation: toastOut 0.3s ease forwards;
}
@keyframes toastIn {
from {
opacity: 0;
transform: translateY(14px);
}
}
@keyframes toastOut {
to {
opacity: 0;
transform: translateY(14px);
}
}
/* ---------- Responsive ---------- */
@media (max-width: 860px) {
.shell {
grid-template-columns: 1fr;
}
.rail {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 16px 24px;
padding: 22px 24px;
}
.rail__lede {
display: none;
}
.stepper {
flex-direction: row;
flex: 1;
gap: 0;
justify-content: space-between;
}
.stepper__item {
flex-direction: column;
text-align: center;
gap: 7px;
flex: 1;
padding: 0;
}
.stepper__item::before {
left: 50%;
right: -50%;
top: 17px;
bottom: auto;
width: auto;
height: 2px;
}
.stepper__txt {
align-items: center;
}
.stepper__sub {
display: none;
}
.rail__secure {
margin: 0;
border: none;
padding: 0;
width: 100%;
justify-content: center;
}
}
@media (max-width: 520px) {
.panel {
padding: 22px 18px 26px;
}
.grid {
grid-template-columns: 1fr;
}
.step__h {
font-size: 21px;
}
.stepper__title {
font-size: 11.5px;
}
.rail {
padding: 18px 16px;
}
.brand__name {
display: none;
}
.actions {
padding-top: 22px;
}
.btn {
padding: 12px 16px;
}
.review__row {
flex-direction: column;
gap: 2px;
}
.review__v {
text-align: left;
}
}(function () {
"use strict";
var form = document.getElementById("wizard");
var steps = Array.prototype.slice.call(form.querySelectorAll(".step"));
var stepperItems = Array.prototype.slice.call(
document.querySelectorAll("#stepper .stepper__item")
);
var progressBar = document.getElementById("progressBar");
var stepCount = document.getElementById("stepCount");
var nextBtn = document.getElementById("nextBtn");
var backBtn = document.getElementById("backBtn");
var reviewEl = document.getElementById("review");
var toaster = document.getElementById("toaster");
var LAST_FORM_STEP = 3; // index of review step
var current = 0;
// ---------- Toast helper ----------
function toast(msg, kind) {
var el = document.createElement("div");
el.className = "toast" + (kind ? " toast--" + kind : "");
el.textContent = msg;
toaster.appendChild(el);
setTimeout(function () {
el.classList.add("is-out");
setTimeout(function () {
el.remove();
}, 300);
}, 2600);
}
// ---------- Validation ----------
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var PHONE_RE = /^[+()\d][\d\s()-]{6,}$/;
function setError(field, message) {
if (!field) return;
field.classList.toggle("is-invalid", !!message);
var err = field.querySelector("[data-err]");
if (err) err.textContent = message || "";
}
function validateControl(ctrl) {
var field = ctrl.closest(".field");
var value = (ctrl.value || "").trim();
var name = ctrl.name;
var label = field
? (field.querySelector(".field__label") || {}).textContent || "This field"
: "This field";
if (!value && ctrl.hasAttribute("required")) {
setError(field, label + " is required.");
return false;
}
if (name === "email" && value && !EMAIL_RE.test(value)) {
setError(field, "Enter a valid email address.");
return false;
}
if (name === "phone" && value && !PHONE_RE.test(value)) {
setError(field, "Enter a valid phone number.");
return false;
}
if (name === "dob" && value) {
var age = (Date.now() - new Date(value).getTime()) / 31557600000;
if (age < 18) {
setError(field, "You must be at least 18 years old.");
return false;
}
}
if (name === "postcode" && value && value.length < 3) {
setError(field, "Enter a valid postal code.");
return false;
}
setError(field, "");
return true;
}
function validateStep(index) {
var section = steps[index];
var ok = true;
// standard inputs/selects
var controls = section.querySelectorAll("input:not([type=checkbox]), select");
Array.prototype.forEach.call(controls, function (ctrl) {
if (!validateControl(ctrl)) ok = false;
});
// identity uploads (step 2)
if (index === 2) {
["id", "selfie"].forEach(function (key) {
var btn = section.querySelector('[data-upload="' + key + '"]');
var err = section.querySelector('[data-err="' + key + '"]');
var done = btn.classList.contains("is-done");
if (err) err.textContent = done ? "" : "Please complete this upload.";
if (!done) ok = false;
});
}
// terms (step 3)
if (index === 3) {
var terms = section.querySelector('input[name="terms"]');
var termsErr = section.querySelector('[data-err="terms"]');
if (!terms.checked) {
if (termsErr) termsErr.textContent = "You must accept the terms to continue.";
ok = false;
} else if (termsErr) {
termsErr.textContent = "";
}
}
return ok;
}
// Clear error as the user fixes a field
form.addEventListener("input", function (e) {
var ctrl = e.target;
if (ctrl.matches("input:not([type=checkbox]), select")) {
var field = ctrl.closest(".field");
if (field && field.classList.contains("is-invalid")) validateControl(ctrl);
}
if (ctrl.name === "terms") {
var termsErr = document.querySelector('[data-err="terms"]');
if (ctrl.checked && termsErr) termsErr.textContent = "";
}
});
// ---------- Step rendering ----------
function showStep(index) {
steps.forEach(function (s, i) {
var active = i === index;
s.classList.toggle("is-active", active);
s.hidden = !active;
});
stepperItems.forEach(function (item, i) {
item.classList.toggle("is-active", i === index);
item.classList.toggle("is-done", i < index && index <= LAST_FORM_STEP);
if (index > LAST_FORM_STEP) item.classList.add("is-done");
});
var pct;
if (index > LAST_FORM_STEP) {
pct = 100;
stepCount.textContent = "Application complete";
} else {
pct = ((index + 1) / (LAST_FORM_STEP + 1)) * 100;
stepCount.textContent = "Step " + (index + 1) + " of " + (LAST_FORM_STEP + 1);
}
progressBar.style.width = pct + "%";
backBtn.hidden = index === 0 || index > LAST_FORM_STEP;
nextBtn.hidden = index > LAST_FORM_STEP;
nextBtn.textContent = index === LAST_FORM_STEP ? "Submit application" : "Continue";
// focus first control of the new step
var firstCtrl = steps[index].querySelector("input, select, button");
if (firstCtrl) {
setTimeout(function () {
firstCtrl.focus({ preventScroll: true });
}, 60);
}
current = index;
}
function goNext() {
if (current <= LAST_FORM_STEP && !validateStep(current)) {
toast("Please fix the highlighted fields.", "err");
var firstBad = steps[current].querySelector(".is-invalid input, .is-invalid select");
if (firstBad) firstBad.focus();
return;
}
if (current < LAST_FORM_STEP) {
if (current === 2) buildReview();
showStep(current + 1);
return;
}
if (current === LAST_FORM_STEP) {
submit();
}
}
function submit() {
nextBtn.disabled = true;
nextBtn.textContent = "Submitting…";
toast("Encrypting & sending your application…");
setTimeout(function () {
var data = collect();
document.getElementById("successName").textContent = data.firstName || "there";
document.getElementById("appRef").textContent =
"NB-" + Math.floor(100000 + Math.random() * 899999);
showStep(LAST_FORM_STEP + 1);
nextBtn.disabled = false;
toast("Account application submitted!", "ok");
}, 1400);
}
// ---------- Data collection ----------
function collect() {
var fd = new FormData(form);
var obj = {};
fd.forEach(function (v, k) {
obj[k] = v;
});
return obj;
}
var COUNTRIES = {
us: "United States",
gb: "United Kingdom",
ca: "Canada",
de: "Germany",
ng: "Nigeria",
au: "Australia"
};
var DOCS = { passport: "Passport", license: "Driver's licence", id: "National ID card" };
function fmtDate(iso) {
if (!iso) return "—";
var d = new Date(iso);
if (isNaN(d)) return iso;
return d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
}
function esc(s) {
return String(s || "—").replace(/[&<>]/g, function (c) {
return { "&": "&", "<": "<", ">": ">" }[c];
});
}
function buildReview() {
var d = collect();
var groups = [
{
step: 0,
title: "Personal details",
rows: [
["Name", esc((d.firstName || "") + " " + (d.lastName || "")).trim() || "—"],
["Email", esc(d.email)],
["Mobile", esc(d.phone)],
["Date of birth", fmtDate(d.dob)]
]
},
{
step: 1,
title: "Home address",
rows: [
["Street", esc(d.street)],
["City", esc(d.city)],
["Postal code", esc(d.postcode)],
["Country", esc(COUNTRIES[d.country] || "—")]
]
},
{
step: 2,
title: "Identity",
rows: [
["Document", esc(DOCS[d.docType] || "—")],
["ID upload", "✓ Verified"],
["Selfie", "✓ Verified"]
]
}
];
reviewEl.innerHTML = groups
.map(function (g) {
var rows = g.rows
.map(function (r) {
return (
'<div class="review__row"><span class="review__k">' +
r[0] +
'</span><span class="review__v">' +
r[1] +
"</span></div>"
);
})
.join("");
return (
'<div class="review__group">' +
'<div class="review__head"><h2>' +
g.title +
'</h2><button type="button" class="review__edit" data-edit="' +
g.step +
'">Edit</button></div>' +
rows +
"</div>"
);
})
.join("");
}
reviewEl.addEventListener("click", function (e) {
var btn = e.target.closest("[data-edit]");
if (btn) showStep(parseInt(btn.getAttribute("data-edit"), 10));
});
// ---------- Upload mock ----------
form.addEventListener("click", function (e) {
var btn = e.target.closest("[data-upload]");
if (!btn || btn.classList.contains("is-uploading")) return;
var sub = btn.querySelector("[data-upload-sub]");
var state = btn.querySelector("[data-upload-state]");
var origSub = btn.dataset.origSub || (btn.dataset.origSub = sub.textContent);
btn.classList.remove("is-done");
btn.classList.add("is-uploading");
var pct = 0;
state.textContent = "0%";
sub.textContent = "Uploading & scanning…";
var timer = setInterval(function () {
pct += Math.floor(8 + Math.random() * 22);
if (pct >= 100) {
pct = 100;
clearInterval(timer);
btn.classList.remove("is-uploading");
btn.classList.add("is-done");
state.textContent = "✓ Verified";
sub.textContent = origSub;
var err = btn.parentNode.querySelector(
'[data-err="' + btn.getAttribute("data-upload") + '"]'
);
if (err) err.textContent = "";
toast("Document verified securely.", "ok");
} else {
state.textContent = pct + "%";
}
}, 280);
});
// ---------- Nav ----------
nextBtn.addEventListener("click", goNext);
backBtn.addEventListener("click", function () {
if (current > 0) showStep(current - 1);
});
// Enter key advances (but not from inside selects/textareas opening)
form.addEventListener("keydown", function (e) {
if (e.key === "Enter" && e.target.tagName !== "BUTTON" && current <= LAST_FORM_STEP) {
e.preventDefault();
goNext();
}
});
showStep(0);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northbridge Bank — Open an Account</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>
<div class="shell">
<!-- Brand rail -->
<aside class="rail" aria-hidden="false">
<div class="brand">
<span class="brand__mark" aria-hidden="true">◆</span>
<span class="brand__name">Northbridge</span>
</div>
<p class="rail__lede">Open your free everyday account in about 5 minutes.</p>
<ol class="stepper" id="stepper" aria-label="Application progress">
<li class="stepper__item is-active" data-step="0">
<span class="stepper__dot">1</span>
<span class="stepper__txt">
<span class="stepper__title">Personal details</span>
<span class="stepper__sub">Your name & contact</span>
</span>
</li>
<li class="stepper__item" data-step="1">
<span class="stepper__dot">2</span>
<span class="stepper__txt">
<span class="stepper__title">Home address</span>
<span class="stepper__sub">Where you live</span>
</span>
</li>
<li class="stepper__item" data-step="2">
<span class="stepper__dot">3</span>
<span class="stepper__txt">
<span class="stepper__title">Identity check</span>
<span class="stepper__sub">ID & selfie</span>
</span>
</li>
<li class="stepper__item" data-step="3">
<span class="stepper__dot">4</span>
<span class="stepper__txt">
<span class="stepper__title">Review & confirm</span>
<span class="stepper__sub">Check everything</span>
</span>
</li>
</ol>
<div class="rail__secure">
<span class="lock" aria-hidden="true">🔒</span>
<span>256-bit encrypted · KYC compliant</span>
</div>
</aside>
<!-- Form panel -->
<main class="panel">
<header class="panel__head">
<div class="panel__progress" role="presentation">
<div class="panel__progress-bar" id="progressBar" style="width: 25%"></div>
</div>
<p class="panel__count" id="stepCount" aria-live="polite">Step 1 of 4</p>
</header>
<form id="wizard" novalidate>
<!-- STEP 1 — Personal details -->
<section class="step is-active" data-panel="0" aria-label="Personal details">
<h1 class="step__h">Let's start with you</h1>
<p class="step__lede">Use your legal name exactly as it appears on your ID.</p>
<div class="grid">
<label class="field">
<span class="field__label">First name</span>
<input name="firstName" type="text" autocomplete="given-name" placeholder="Mara" required />
<span class="field__err" data-err></span>
</label>
<label class="field">
<span class="field__label">Last name</span>
<input name="lastName" type="text" autocomplete="family-name" placeholder="Okonkwo" required />
<span class="field__err" data-err></span>
</label>
<label class="field field--full">
<span class="field__label">Email address</span>
<input name="email" type="email" autocomplete="email" placeholder="[email protected]" required />
<span class="field__err" data-err></span>
</label>
<label class="field">
<span class="field__label">Mobile number</span>
<input name="phone" type="tel" inputmode="tel" autocomplete="tel" placeholder="+1 555 0142" required />
<span class="field__err" data-err></span>
</label>
<label class="field">
<span class="field__label">Date of birth</span>
<input name="dob" type="date" autocomplete="bday" max="2008-06-16" required />
<span class="field__err" data-err></span>
</label>
</div>
</section>
<!-- STEP 2 — Address -->
<section class="step" data-panel="1" aria-label="Home address" hidden>
<h1 class="step__h">Where do you live?</h1>
<p class="step__lede">We use this to verify your residency. No PO boxes.</p>
<div class="grid">
<label class="field field--full">
<span class="field__label">Street address</span>
<input name="street" type="text" autocomplete="address-line1" placeholder="48 Harbour Lane" required />
<span class="field__err" data-err></span>
</label>
<label class="field">
<span class="field__label">City</span>
<input name="city" type="text" autocomplete="address-level2" placeholder="Brightwater" required />
<span class="field__err" data-err></span>
</label>
<label class="field">
<span class="field__label">Postal code</span>
<input name="postcode" type="text" autocomplete="postal-code" placeholder="BW3 9QA" required />
<span class="field__err" data-err></span>
</label>
<label class="field field--full">
<span class="field__label">Country</span>
<select name="country" required>
<option value="">Select a country…</option>
<option value="us">United States</option>
<option value="gb">United Kingdom</option>
<option value="ca">Canada</option>
<option value="de">Germany</option>
<option value="ng">Nigeria</option>
<option value="au">Australia</option>
</select>
<span class="field__err" data-err></span>
</label>
</div>
</section>
<!-- STEP 3 — Identity -->
<section class="step" data-panel="2" aria-label="Identity check" hidden>
<h1 class="step__h">Verify your identity</h1>
<p class="step__lede">Upload a government ID and a quick selfie. Files stay encrypted.</p>
<div class="grid">
<label class="field field--full">
<span class="field__label">Document type</span>
<select name="docType" required>
<option value="">Choose document…</option>
<option value="passport">Passport</option>
<option value="license">Driver's licence</option>
<option value="id">National ID card</option>
</select>
<span class="field__err" data-err></span>
</label>
</div>
<div class="uploads">
<button type="button" class="upload" data-upload="id">
<span class="upload__icon" aria-hidden="true">🪪</span>
<span class="upload__txt">
<span class="upload__title" data-upload-title>Upload your ID</span>
<span class="upload__sub" data-upload-sub>Front side · JPG or PNG</span>
</span>
<span class="upload__state" data-upload-state></span>
</button>
<span class="field__err" data-err="id"></span>
<button type="button" class="upload" data-upload="selfie">
<span class="upload__icon" aria-hidden="true">🤳</span>
<span class="upload__txt">
<span class="upload__title" data-upload-title>Take a selfie</span>
<span class="upload__sub" data-upload-sub>Face the camera in good light</span>
</span>
<span class="upload__state" data-upload-state></span>
</button>
<span class="field__err" data-err="selfie"></span>
</div>
<p class="note">
<span class="lock" aria-hidden="true">🔒</span>
This is a mock upload — no files leave your browser.
</p>
</section>
<!-- STEP 4 — Review -->
<section class="step" data-panel="3" aria-label="Review and confirm" hidden>
<h1 class="step__h">Review your application</h1>
<p class="step__lede">Check everything looks right before you submit.</p>
<div class="review" id="review"></div>
<label class="check">
<input type="checkbox" name="terms" required />
<span
>I confirm the details above are accurate and accept the
<a href="#" onclick="return false">Account Terms</a> and
<a href="#" onclick="return false">Privacy Policy</a>.</span
>
</label>
<span class="field__err" data-err="terms"></span>
</section>
<!-- Success -->
<section class="step success" data-panel="4" aria-label="Application complete" hidden>
<div class="success__badge" aria-hidden="true">✓</div>
<h1 class="step__h">You're all set, <span id="successName">there</span>!</h1>
<p class="step__lede">
Your account application has been received and is being reviewed.
</p>
<div class="success__card">
<div class="success__row">
<span class="success__k">Application ref</span>
<span class="success__v mono" id="appRef">NB-•••••</span>
</div>
<div class="success__row">
<span class="success__k">New account (IBAN)</span>
<span class="success__v mono">GB29 NB00 0000 0000 4242</span>
</div>
<div class="success__row">
<span class="success__k">Status</span>
<span class="pill pill--pending">In review</span>
</div>
</div>
<p class="note success__note">We'll email you within 1 business day.</p>
</section>
<!-- Nav -->
<footer class="actions" id="actions">
<button type="button" class="btn btn--ghost" id="backBtn" hidden>← Back</button>
<button type="button" class="btn btn--primary" id="nextBtn">Continue</button>
</footer>
</form>
</main>
</div>
<div class="toaster" id="toaster" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Account-Open Wizard
A calm, four-step onboarding flow for a fictional retail bank, Northbridge. A dark brand rail on the left tracks progress with a vertical stepper whose dots flip from blue (active) to teal checkmarks (done), while a gradient progress bar and a “Step N of 4” label sit above the form. Customers move through personal details, home address, an identity check, and a final review — each step gated by inline validation so they cannot skip ahead with missing or malformed data.
Validation feels human: required fields, a real email pattern, a phone format check, a minimum-age rule on date of birth, and per-field error messages that clear themselves as soon as the input is corrected. The identity step swaps file pickers for a mock ID and selfie upload, each animating through a scanning percentage before settling on a green “Verified” state with a toast. The review step rebuilds an editable summary of everything entered — every group has an Edit shortcut that jumps straight back to that step — and a terms checkbox must be ticked before submitting.
Submitting shows an encrypting toast, then a celebratory success screen with a generated application reference, a masked IBAN (GB29 NB00 … 4242), and an “In review” status pill. The whole interface is self-contained vanilla JavaScript — a small toast() helper, keyboard support (Enter advances, focus moves to each new step), tabular money figures, security cues like lock icons and an encrypted badge, and a layout that folds the stepper into a horizontal strip and collapses to a single column down to 360px-wide screens.
Illustrative UI only — not real banking software or financial advice.