Dating — Onboarding Flow
A five-step dating app signup wrapped in a 430px phone frame: verify a phone number, add name and age, upload up to six photos, pick interest tags, and choose what you are looking for. Progress dots track every step, inline validation blocks bad input, back and next keep state, and a confetti welcome screen celebrates the finish.
MCP
Code
:root {
--bg: #fff5f6;
--surface: #ffffff;
--ink: #2a1a2e;
--ink-2: #5a4a55;
--muted: #8a7a85;
--coral: #ff5e6c;
--coral-d: #e63950;
--violet: #8b5cf6;
--violet-d: #7c3aed;
--pink: #ff8fb1;
--grad: linear-gradient(135deg, #ff5e6c, #8b5cf6);
--line: rgba(42, 26, 46, 0.10);
--white: #fff;
--r-sm: 12px;
--r-md: 20px;
--r-lg: 28px;
--shadow: 0 18px 50px -18px rgba(124, 58, 237, 0.45);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
}
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--ink);
background:
radial-gradient(1100px 600px at 15% -10%, rgba(255, 94, 108, 0.18), transparent 60%),
radial-gradient(900px 600px at 100% 110%, rgba(139, 92, 246, 0.20), transparent 60%),
var(--bg);
min-height: 100vh;
}
.stage {
min-height: 100vh;
display: grid;
place-items: center;
padding: 32px 16px;
}
/* ---------- Phone frame ---------- */
.phone {
position: relative;
width: 100%;
max-width: 430px;
height: min(88vh, 900px);
background: var(--surface);
border-radius: 44px;
box-shadow: var(--shadow), 0 0 0 10px #1b0f21, 0 0 0 12px #2a1a2e;
overflow: hidden;
display: flex;
flex-direction: column;
}
.notch {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
width: 130px;
height: 26px;
background: #1b0f21;
border-radius: 0 0 16px 16px;
z-index: 6;
}
/* ---------- Top bar ---------- */
.topbar {
position: relative;
display: flex;
align-items: center;
gap: 10px;
padding: 40px 20px 12px;
min-height: 78px;
}
.icon-btn {
position: absolute;
left: 16px;
top: 40px;
display: grid;
place-items: center;
width: 38px;
height: 38px;
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink);
border-radius: 50%;
cursor: pointer;
transition: transform .15s ease, background .15s ease, box-shadow .15s ease;
}
.icon-btn:hover { background: #fff0f2; transform: translateX(-2px); }
.icon-btn:active { transform: scale(.92); }
.icon-btn:focus-visible { outline: 3px solid rgba(139, 92, 246, .45); outline-offset: 2px; }
.brand {
position: absolute;
right: 20px;
top: 46px;
font-family: "Poppins", sans-serif;
font-weight: 800;
font-size: 15px;
background: var(--grad);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
letter-spacing: .3px;
}
.dots {
display: flex;
gap: 8px;
margin: 0 auto;
padding: 0;
}
.dot {
width: 9px;
height: 9px;
border-radius: 50%;
border: none;
padding: 0;
background: var(--line);
cursor: pointer;
transition: width .25s ease, background .25s ease;
}
.dot.done { background: var(--violet); }
.dot.active {
width: 26px;
border-radius: 6px;
background: var(--grad);
}
.dot:disabled { cursor: default; }
.dot:focus-visible { outline: 3px solid rgba(139, 92, 246, .45); outline-offset: 2px; }
/* ---------- Viewport / steps ---------- */
.viewport {
flex: 1;
overflow-y: auto;
padding: 8px 24px 20px;
scrollbar-width: thin;
}
.viewport::-webkit-scrollbar { width: 6px; }
.viewport::-webkit-scrollbar-thumb { background: var(--line); border-radius: 99px; }
.step { display: none; animation: slideIn .35s ease both; }
.step.is-active { display: block; }
@keyframes slideIn {
from { opacity: 0; transform: translateX(18px); }
to { opacity: 1; transform: translateX(0); }
}
.logo {
width: 58px;
height: 58px;
display: grid;
place-items: center;
font-size: 28px;
border-radius: var(--r-md);
background: linear-gradient(135deg, rgba(255, 94, 108, .16), rgba(139, 92, 246, .16));
margin-bottom: 14px;
}
.title {
font-family: "Poppins", sans-serif;
font-size: 25px;
font-weight: 800;
margin: 0 0 6px;
letter-spacing: -.4px;
color: var(--ink);
}
.title.big { font-size: 27px; }
#finishName {
background: var(--grad);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.sub {
margin: 0 0 22px;
color: var(--ink-2);
font-size: 14.5px;
}
/* ---------- Fields ---------- */
.field { display: block; margin-bottom: 18px; }
.field-label {
display: block;
font-size: 12.5px;
font-weight: 700;
color: var(--ink-2);
margin-bottom: 7px;
text-transform: uppercase;
letter-spacing: .4px;
}
input[type="text"],
input[type="tel"],
input[type="date"] {
width: 100%;
font: inherit;
font-size: 16px;
padding: 14px 16px;
border: 1.5px solid var(--line);
border-radius: var(--r-sm);
background: var(--surface);
color: var(--ink);
transition: border-color .18s ease, box-shadow .18s ease;
}
input::placeholder { color: var(--muted); }
input:focus {
outline: none;
border-color: var(--violet);
box-shadow: 0 0 0 4px rgba(139, 92, 246, .16);
}
input.invalid {
border-color: var(--coral-d);
box-shadow: 0 0 0 4px rgba(230, 57, 80, .14);
}
.phone-input {
display: flex;
align-items: center;
border: 1.5px solid var(--line);
border-radius: var(--r-sm);
overflow: hidden;
transition: border-color .18s ease, box-shadow .18s ease;
}
.phone-input:focus-within {
border-color: var(--violet);
box-shadow: 0 0 0 4px rgba(139, 92, 246, .16);
}
.phone-input .cc {
padding: 14px 12px 14px 16px;
font-weight: 700;
color: var(--ink);
background: #fff0f2;
border-right: 1.5px solid var(--line);
white-space: nowrap;
}
.phone-input input {
border: none;
border-radius: 0;
flex: 1;
}
.phone-input input:focus { box-shadow: none; }
.hint {
display: block;
font-size: 12.5px;
color: var(--muted);
margin-top: 7px;
}
.hint.err { color: var(--coral-d); font-weight: 600; }
/* ---------- OTP ---------- */
.verify {
margin-top: 8px;
padding: 16px;
border-radius: var(--r-md);
background: #fbf5ff;
border: 1.5px solid rgba(139, 92, 246, .2);
animation: slideIn .3s ease both;
}
.otp { display: flex; gap: 10px; margin: 8px 0; }
.otp-box {
width: 100%;
text-align: center;
font-size: 22px;
font-weight: 700;
padding: 12px 0;
}
/* ---------- Age badge ---------- */
.age-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 999px;
background: linear-gradient(135deg, rgba(255, 94, 108, .14), rgba(139, 92, 246, .14));
color: var(--violet-d);
font-weight: 700;
font-size: 13.5px;
}
/* ---------- Photo grid ---------- */
.photo-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 14px;
}
.photo-slot {
position: relative;
aspect-ratio: 3 / 4;
border-radius: var(--r-md);
border: 2px dashed var(--line);
background: #fff;
display: grid;
place-items: center;
cursor: pointer;
overflow: hidden;
color: var(--muted);
font-size: 26px;
transition: transform .15s ease, border-color .15s ease, box-shadow .15s ease;
}
.photo-slot:hover { border-color: var(--violet); transform: translateY(-2px); }
.photo-slot:focus-visible { outline: 3px solid rgba(139, 92, 246, .45); outline-offset: 2px; }
.photo-slot.filled {
border: none;
color: #fff;
box-shadow: 0 10px 22px -10px rgba(124, 58, 237, .5);
}
.photo-slot.filled::after {
content: "✕";
position: absolute;
top: 6px;
right: 6px;
width: 24px;
height: 24px;
display: grid;
place-items: center;
font-size: 13px;
background: rgba(27, 15, 33, .55);
border-radius: 50%;
color: #fff;
}
.photo-slot .tag {
position: absolute;
bottom: 7px;
left: 8px;
font-size: 11px;
font-weight: 700;
text-shadow: 0 1px 4px rgba(0, 0, 0, .35);
}
.photo-slot.main-badge .tag::before {
content: "★ Main · ";
}
.counter {
font-size: 13px;
color: var(--muted);
font-weight: 600;
margin: 2px 0 0;
}
.counter span { color: var(--violet-d); }
/* ---------- Chips ---------- */
.chips { display: flex; flex-wrap: wrap; gap: 9px; margin-bottom: 14px; }
.chip {
font: inherit;
font-size: 14px;
font-weight: 600;
padding: 9px 15px;
border-radius: 999px;
border: 1.5px solid var(--line);
background: var(--surface);
color: var(--ink-2);
cursor: pointer;
transition: all .15s ease;
}
.chip:hover { border-color: var(--pink); transform: translateY(-1px); }
.chip:focus-visible { outline: 3px solid rgba(139, 92, 246, .45); outline-offset: 2px; }
.chip[aria-pressed="true"] {
background: var(--grad);
color: #fff;
border-color: transparent;
box-shadow: 0 8px 18px -9px rgba(230, 57, 80, .55);
}
/* ---------- Intent cards ---------- */
.intents { display: grid; gap: 12px; }
.intent {
display: flex;
align-items: center;
gap: 14px;
text-align: left;
width: 100%;
font: inherit;
padding: 15px 16px;
border-radius: var(--r-md);
border: 1.5px solid var(--line);
background: var(--surface);
cursor: pointer;
transition: all .16s ease;
}
.intent:hover { transform: translateY(-2px); border-color: var(--pink); }
.intent:focus-visible { outline: 3px solid rgba(139, 92, 246, .45); outline-offset: 2px; }
.intent .emo {
font-size: 26px;
width: 46px;
height: 46px;
display: grid;
place-items: center;
border-radius: 14px;
background: linear-gradient(135deg, rgba(255, 94, 108, .14), rgba(139, 92, 246, .14));
flex: none;
}
.intent .txt strong {
display: block;
font-family: "Poppins", sans-serif;
font-size: 15.5px;
color: var(--ink);
}
.intent .txt small { color: var(--muted); font-size: 13px; }
.intent .radio {
margin-left: auto;
width: 22px;
height: 22px;
border-radius: 50%;
border: 2px solid var(--line);
flex: none;
transition: all .16s ease;
}
.intent[aria-checked="true"] {
border-color: var(--violet);
background: linear-gradient(135deg, rgba(255, 94, 108, .07), rgba(139, 92, 246, .09));
}
.intent[aria-checked="true"] .radio {
border-color: var(--violet);
background: var(--violet);
box-shadow: inset 0 0 0 4px #fff;
}
/* ---------- Finish ---------- */
.finish { text-align: center; padding-top: 12px; }
.spark-burst {
font-size: 56px;
margin: 8px 0 6px;
animation: pop .6s cubic-bezier(.2, 1.4, .4, 1) both;
}
@keyframes pop {
0% { transform: scale(0) rotate(-30deg); opacity: 0; }
100% { transform: scale(1) rotate(0); opacity: 1; }
}
.finish .sub { margin-bottom: 20px; }
.summary {
list-style: none;
margin: 0 0 22px;
padding: 0;
text-align: left;
display: grid;
gap: 10px;
}
.summary li {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 12px 15px;
border-radius: var(--r-sm);
background: #fbf5ff;
border: 1px solid rgba(139, 92, 246, .14);
font-size: 14px;
}
.summary li span:first-child { color: var(--muted); font-weight: 600; }
.summary li span:last-child { color: var(--ink); font-weight: 700; text-align: right; }
.confetti {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.confetti i {
position: absolute;
top: -12px;
width: 9px;
height: 14px;
border-radius: 2px;
opacity: .9;
animation: fall linear forwards;
}
@keyframes fall {
to { transform: translateY(120vh) rotate(540deg); opacity: 0; }
}
/* ---------- Footer ---------- */
.footer {
padding: 12px 24px calc(18px + env(safe-area-inset-bottom));
border-top: 1px solid var(--line);
background: var(--surface);
}
.btn {
font: inherit;
font-weight: 700;
border: none;
border-radius: 999px;
cursor: pointer;
transition: transform .15s ease, box-shadow .15s ease, opacity .15s ease;
}
.btn-primary {
background: var(--grad);
color: #fff;
padding: 15px 20px;
font-size: 15.5px;
box-shadow: 0 12px 26px -10px rgba(230, 57, 80, .6);
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 16px 30px -10px rgba(230, 57, 80, .7); }
.btn-primary:active { transform: translateY(0) scale(.98); }
.btn-primary:focus-visible { outline: 3px solid rgba(139, 92, 246, .5); outline-offset: 2px; }
.btn.block { width: 100%; display: block; }
.btn:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; transform: none; }
.legal {
text-align: center;
font-size: 11.5px;
color: var(--muted);
margin: 10px 0 0;
}
.legal a { color: var(--violet-d); text-decoration: none; font-weight: 600; }
.legal a:hover { text-decoration: underline; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 34px;
transform: translate(-50%, 30px);
background: var(--ink);
color: #fff;
padding: 12px 20px;
border-radius: 999px;
font-size: 13.5px;
font-weight: 600;
box-shadow: 0 16px 34px -12px rgba(0, 0, 0, .5);
opacity: 0;
pointer-events: none;
transition: opacity .25s ease, transform .25s ease;
z-index: 40;
max-width: 88vw;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.stage { padding: 0; }
.phone {
max-width: 100%;
height: 100vh;
height: 100dvh;
border-radius: 0;
box-shadow: none;
}
.notch { display: none; }
.topbar { padding-top: 20px; min-height: 60px; }
.icon-btn { top: 20px; }
.brand { top: 26px; }
.viewport { padding-left: 18px; padding-right: 18px; }
.footer { padding-left: 18px; padding-right: 18px; }
.title { font-size: 23px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}(function () {
"use strict";
var TOTAL = 5; // interactive steps (0..4); step 5 is the finish screen
var current = 0;
var phoneVerified = false;
var state = {
phone: "",
name: "",
dob: "",
age: null,
photos: [], // indexes 0..5, true = filled
interests: [],
intent: ""
};
// ---------- helpers ----------
var $ = function (sel, ctx) { return (ctx || document).querySelector(sel); };
var $$ = function (sel, ctx) { return Array.prototype.slice.call((ctx || document).querySelectorAll(sel)); };
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2400);
}
var steps = $$(".step");
var dotsWrap = $("#dots");
var backBtn = $("#backBtn");
var nextBtn = $("#nextBtn");
var footer = $("#footer");
// ---------- progress dots ----------
var dots = [];
for (var i = 0; i < TOTAL; i++) {
var d = document.createElement("button");
d.type = "button";
d.className = "dot";
d.setAttribute("role", "tab");
d.setAttribute("aria-label", "Step " + (i + 1) + " of " + TOTAL);
(function (idx) {
d.addEventListener("click", function () {
if (idx < current) goTo(idx); // only jump back to completed steps
});
})(i);
dotsWrap.appendChild(d);
dots.push(d);
}
function renderDots() {
dots.forEach(function (dot, idx) {
dot.classList.toggle("active", idx === current);
dot.classList.toggle("done", idx < current);
dot.disabled = idx >= current;
if (idx === current) dot.setAttribute("aria-selected", "true");
else dot.removeAttribute("aria-selected");
});
}
// ---------- navigation ----------
var CTA = ["Continue", "Continue", "Continue", "Continue", "Create account"];
function show(idx) {
steps.forEach(function (s) {
var n = Number(s.getAttribute("data-step"));
var on = n === idx;
s.classList.toggle("is-active", on);
s.hidden = !on;
});
}
function goTo(idx) {
current = idx;
show(idx);
renderDots();
backBtn.hidden = idx === 0;
nextBtn.textContent = CTA[idx] || "Continue";
$(".viewport").scrollTop = 0;
// focus the first control for keyboard users
var focusable = steps[idx].querySelector("input, button, .chip, .intent, .photo-slot");
if (focusable) setTimeout(function () { focusable.focus({ preventScroll: true }); }, 60);
}
backBtn.addEventListener("click", function () {
if (current > 0) goTo(current - 1);
});
nextBtn.addEventListener("click", function () {
if (validate(current)) {
if (current < TOTAL - 1) {
goTo(current + 1);
} else {
finish();
}
}
});
// ============================================================
// STEP 1 — Phone
// ============================================================
var phoneInput = $("#phone");
var verifyBlock = $("#verifyBlock");
var otpBoxes = $$(".otp-box");
function formatPhone(raw) {
var d = raw.replace(/\D/g, "").slice(0, 10);
if (d.length > 6) return "(" + d.slice(0, 3) + ") " + d.slice(3, 6) + "-" + d.slice(6);
if (d.length > 3) return "(" + d.slice(0, 3) + ") " + d.slice(3);
if (d.length > 0) return "(" + d;
return "";
}
phoneInput.addEventListener("input", function () {
phoneInput.value = formatPhone(phoneInput.value);
phoneInput.classList.remove("invalid");
var digits = phoneInput.value.replace(/\D/g, "");
if (digits.length === 10 && verifyBlock.hidden) {
verifyBlock.hidden = false;
toast("Code sent! Use 4821 for this demo.");
setTimeout(function () { otpBoxes[0].focus(); }, 150);
}
});
otpBoxes.forEach(function (box, i) {
box.addEventListener("input", function () {
box.value = box.value.replace(/\D/g, "");
if (box.value && i < otpBoxes.length - 1) otpBoxes[i + 1].focus();
checkOtp();
});
box.addEventListener("keydown", function (e) {
if (e.key === "Backspace" && !box.value && i > 0) otpBoxes[i - 1].focus();
});
});
function checkOtp() {
var code = otpBoxes.map(function (b) { return b.value; }).join("");
if (code.length === 4) {
if (code === "4821") {
phoneVerified = true;
toast("Number verified 🎉");
} else {
phoneVerified = false;
}
}
}
// ============================================================
// STEP 2 — Name + age
// ============================================================
var nameInput = $("#name");
var dobInput = $("#dob");
var ageBadge = $("#ageBadge");
// constrain the date picker to a plausible range
(function () {
var today = new Date();
var max = new Date(today.getFullYear() - 18, today.getMonth(), today.getDate());
var min = new Date(today.getFullYear() - 100, today.getMonth(), today.getDate());
dobInput.max = max.toISOString().slice(0, 10);
dobInput.min = min.toISOString().slice(0, 10);
})();
function ageFrom(dobStr) {
if (!dobStr) return null;
var b = new Date(dobStr);
if (isNaN(b)) return null;
var t = new Date();
var a = t.getFullYear() - b.getFullYear();
var m = t.getMonth() - b.getMonth();
if (m < 0 || (m === 0 && t.getDate() < b.getDate())) a--;
return a;
}
nameInput.addEventListener("input", function () { nameInput.classList.remove("invalid"); });
dobInput.addEventListener("change", function () {
dobInput.classList.remove("invalid");
var a = ageFrom(dobInput.value);
if (a != null && a >= 18 && a <= 100) {
ageBadge.hidden = false;
ageBadge.textContent = "You're " + a + " — you'll appear as " + a;
} else {
ageBadge.hidden = true;
}
});
// ============================================================
// STEP 3 — Photos
// ============================================================
var photoGrid = $("#photoGrid");
var photoCount = $("#photoCount");
var GRADS = [
"linear-gradient(135deg,#ff5e6c,#ff8fb1)",
"linear-gradient(135deg,#8b5cf6,#c4b5fd)",
"linear-gradient(135deg,#ff8fb1,#8b5cf6)",
"linear-gradient(135deg,#e63950,#7c3aed)",
"linear-gradient(135deg,#ff5e6c,#7c3aed)",
"linear-gradient(135deg,#ff8fb1,#ff5e6c)"
];
var photoFilled = [false, false, false, false, false, false];
for (var p = 0; p < 6; p++) {
var slot = document.createElement("button");
slot.type = "button";
slot.className = "photo-slot";
slot.dataset.idx = p;
slot.setAttribute("aria-label", "Photo slot " + (p + 1) + ", empty");
slot.textContent = "+";
(function (idx, el) {
el.addEventListener("click", function () { togglePhoto(idx, el); });
})(p, slot);
photoGrid.appendChild(slot);
}
function togglePhoto(idx, el) {
photoFilled[idx] = !photoFilled[idx];
if (photoFilled[idx]) {
el.classList.add("filled");
el.style.background = GRADS[idx];
el.textContent = "";
var tag = document.createElement("span");
tag.className = "tag";
tag.textContent = "Photo " + (idx + 1);
el.appendChild(tag);
el.setAttribute("aria-label", "Photo slot " + (idx + 1) + ", filled. Activate to remove.");
} else {
el.classList.remove("filled", "main-badge");
el.style.background = "";
el.textContent = "+";
el.setAttribute("aria-label", "Photo slot " + (idx + 1) + ", empty");
}
updatePhotoState();
}
function updatePhotoState() {
state.photos = [];
photoFilled.forEach(function (f, i) { if (f) state.photos.push(i); });
photoCount.textContent = state.photos.length;
// mark the first filled slot as the main photo
$$(".photo-slot").forEach(function (el) { el.classList.remove("main-badge"); });
if (state.photos.length) {
$$(".photo-slot")[state.photos[0]].classList.add("main-badge");
}
}
// ============================================================
// STEP 4 — Interests
// ============================================================
var INTERESTS = [
"🎵 Live music", "🏔️ Hiking", "☕ Coffee", "🎨 Art", "🍜 Foodie",
"📚 Reading", "🎮 Gaming", "🐶 Dogs", "✈️ Travel", "🧘 Yoga",
"🎬 Film", "🌱 Plants", "🏄 Surfing", "🍷 Wine", "🎧 Podcasts",
"📷 Photography", "🏀 Sports", "🍳 Cooking"
];
var chipsWrap = $("#chips");
var chipCount = $("#chipCount");
INTERESTS.forEach(function (label) {
var c = document.createElement("button");
c.type = "button";
c.className = "chip";
c.textContent = label;
c.setAttribute("aria-pressed", "false");
c.addEventListener("click", function () {
var on = c.getAttribute("aria-pressed") === "true";
if (!on && state.interests.length >= 8) {
toast("Pick up to 8 interests");
return;
}
c.setAttribute("aria-pressed", String(!on));
var name = label.replace(/^\S+\s/, "");
if (!on) state.interests.push(name);
else state.interests = state.interests.filter(function (x) { return x !== name; });
chipCount.textContent = state.interests.length;
});
chipsWrap.appendChild(c);
});
// ============================================================
// STEP 5 — Intentions
// ============================================================
var INTENTS = [
{ emo: "💕", title: "A relationship", desc: "Looking for something long-term" },
{ emo: "✨", title: "Something casual", desc: "Keeping it light and fun" },
{ emo: "🤝", title: "New friends", desc: "Expanding my circle" },
{ emo: "🤔", title: "Still figuring it out", desc: "Open to seeing where it goes" }
];
var intentsWrap = $("#intents");
INTENTS.forEach(function (it) {
var card = document.createElement("button");
card.type = "button";
card.className = "intent";
card.setAttribute("role", "radio");
card.setAttribute("aria-checked", "false");
card.innerHTML =
'<span class="emo">' + it.emo + '</span>' +
'<span class="txt"><strong>' + it.title + '</strong><small>' + it.desc + '</small></span>' +
'<span class="radio" aria-hidden="true"></span>';
card.addEventListener("click", function () {
$$(".intent").forEach(function (c) { c.setAttribute("aria-checked", "false"); });
card.setAttribute("aria-checked", "true");
state.intent = it.title;
});
intentsWrap.appendChild(card);
});
// arrow-key navigation for the radiogroup
intentsWrap.addEventListener("keydown", function (e) {
var cards = $$(".intent");
var idx = cards.indexOf(document.activeElement);
if (idx < 0) return;
if (e.key === "ArrowDown" || e.key === "ArrowRight") {
e.preventDefault();
cards[(idx + 1) % cards.length].focus();
} else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
e.preventDefault();
cards[(idx - 1 + cards.length) % cards.length].focus();
} else if (e.key === " " || e.key === "Enter") {
e.preventDefault();
document.activeElement.click();
}
});
// ============================================================
// Validation per step
// ============================================================
function validate(step) {
if (step === 0) {
var digits = phoneInput.value.replace(/\D/g, "");
if (digits.length !== 10) {
phoneInput.classList.add("invalid");
toast("Enter a valid 10-digit number");
phoneInput.focus();
return false;
}
if (!phoneVerified) {
toast("Enter the code 4821 to verify");
if (!verifyBlock.hidden) otpBoxes[0].focus();
return false;
}
state.phone = phoneInput.value;
return true;
}
if (step === 1) {
var nm = nameInput.value.trim();
if (!/^[A-Za-zÀ-ſ' -]{2,20}$/.test(nm)) {
nameInput.classList.add("invalid");
toast("Enter your name (letters only)");
nameInput.focus();
return false;
}
var a = ageFrom(dobInput.value);
if (a == null) {
dobInput.classList.add("invalid");
toast("Add your date of birth");
dobInput.focus();
return false;
}
if (a < 18) {
dobInput.classList.add("invalid");
toast("You must be 18 or older");
return false;
}
state.name = nm;
state.dob = dobInput.value;
state.age = a;
return true;
}
if (step === 2) {
if (state.photos.length < 2) {
toast("Add at least 2 photos");
return false;
}
return true;
}
if (step === 3) {
if (state.interests.length < 3) {
toast("Pick at least 3 interests");
return false;
}
return true;
}
if (step === 4) {
if (!state.intent) {
toast("Choose what you're looking for");
return false;
}
return true;
}
return true;
}
// ============================================================
// Finish
// ============================================================
function finish() {
$("#finishName").textContent = state.name || "friend";
var summary = $("#summary");
var rows = [
["Name", state.name + ", " + state.age],
["Phone", state.phone],
["Photos", state.photos.length + " added"],
["Interests", state.interests.slice(0, 3).join(", ") + (state.interests.length > 3 ? " +" + (state.interests.length - 3) : "")],
["Looking for", state.intent]
];
summary.innerHTML = rows.map(function (r) {
return "<li><span>" + r[0] + "</span><span>" + r[1] + "</span></li>";
}).join("");
// hide the sticky footer, show finish step (step index 5)
footer.style.display = "none";
backBtn.hidden = true;
steps.forEach(function (s) {
var n = Number(s.getAttribute("data-step"));
var on = n === 5;
s.classList.toggle("is-active", on);
s.hidden = !on;
});
dots.forEach(function (d) { d.classList.remove("active"); d.classList.add("done"); });
launchConfetti();
toast("Welcome to Spark, " + state.name + "!");
}
function launchConfetti() {
var box = $("#confetti");
box.innerHTML = "";
var colors = ["#ff5e6c", "#8b5cf6", "#ff8fb1", "#7c3aed", "#e63950", "#ffd166"];
for (var i = 0; i < 44; i++) {
var piece = document.createElement("i");
piece.style.left = Math.random() * 100 + "%";
piece.style.background = colors[i % colors.length];
piece.style.animationDuration = (2.4 + Math.random() * 2) + "s";
piece.style.animationDelay = (Math.random() * 0.6) + "s";
piece.style.transform = "translateY(0) rotate(" + (Math.random() * 360) + "deg)";
box.appendChild(piece);
}
}
$("#enterBtn").addEventListener("click", function () {
toast("Loading your matches… 💘");
});
// ---------- init ----------
goTo(0);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Spark — Create your 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&family=Poppins:wght@600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="stage">
<div class="phone" role="application" aria-label="Spark onboarding">
<div class="notch" aria-hidden="true"></div>
<!-- Top bar: back + progress dots -->
<header class="topbar">
<button class="icon-btn" id="backBtn" type="button" aria-label="Go back" hidden>
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="dots" id="dots" role="tablist" aria-label="Onboarding steps"></div>
<span class="brand" aria-hidden="true">Spark</span>
</header>
<!-- Scrollable step area -->
<div class="viewport">
<form id="flow" novalidate>
<!-- STEP 1: Phone -->
<section class="step is-active" data-step="0" aria-label="Phone number">
<div class="logo" aria-hidden="true">✷</div>
<h1 class="title">What's your number?</h1>
<p class="sub">We'll text a code to keep your account secure. Standard rates may apply.</p>
<label class="field">
<span class="field-label">Phone number</span>
<div class="phone-input">
<span class="cc">🇺🇸 +1</span>
<input id="phone" name="phone" type="tel" inputmode="numeric" autocomplete="tel"
placeholder="(555) 000-1234" aria-describedby="phoneHint" maxlength="14" />
</div>
<span class="hint" id="phoneHint">Enter a 10-digit US mobile number.</span>
</label>
<div class="verify" id="verifyBlock" hidden>
<span class="field-label">Enter the 4-digit code</span>
<div class="otp" id="otp">
<input class="otp-box" inputmode="numeric" maxlength="1" aria-label="Digit 1" />
<input class="otp-box" inputmode="numeric" maxlength="1" aria-label="Digit 2" />
<input class="otp-box" inputmode="numeric" maxlength="1" aria-label="Digit 3" />
<input class="otp-box" inputmode="numeric" maxlength="1" aria-label="Digit 4" />
</div>
<span class="hint">Demo code is <strong>4821</strong>.</span>
</div>
</section>
<!-- STEP 2: Name + age -->
<section class="step" data-step="1" aria-label="Name and age" hidden>
<div class="logo" aria-hidden="true">👋</div>
<h1 class="title">Nice to meet you</h1>
<p class="sub">This is how you'll appear to others. You can't change your age later.</p>
<label class="field">
<span class="field-label">First name</span>
<input id="name" name="name" type="text" autocomplete="given-name"
placeholder="e.g. Mara" maxlength="20" aria-describedby="nameHint" />
<span class="hint" id="nameHint">Letters only, 2–20 characters.</span>
</label>
<label class="field">
<span class="field-label">Date of birth</span>
<input id="dob" name="dob" type="date" aria-describedby="dobHint" />
<span class="hint" id="dobHint">You must be 18 or older to join Spark.</span>
</label>
<div class="age-badge" id="ageBadge" hidden></div>
</section>
<!-- STEP 3: Photos -->
<section class="step" data-step="2" aria-label="Photos" hidden>
<div class="logo" aria-hidden="true">📸</div>
<h1 class="title">Add your best photos</h1>
<p class="sub">Add at least 2. Tap a tile to add a photo, tap again to remove it.</p>
<div class="photo-grid" id="photoGrid" aria-label="Photo slots"></div>
<p class="counter"><span id="photoCount">0</span> / 6 photos added</p>
</section>
<!-- STEP 4: Interests -->
<section class="step" data-step="3" aria-label="Interests" hidden>
<div class="logo" aria-hidden="true">✨</div>
<h1 class="title">What are you into?</h1>
<p class="sub">Pick at least 3. These help us find people you'll click with.</p>
<div class="chips" id="chips" role="group" aria-label="Interests"></div>
<p class="counter"><span id="chipCount">0</span> selected</p>
</section>
<!-- STEP 5: Intentions -->
<section class="step" data-step="4" aria-label="Dating intentions" hidden>
<div class="logo" aria-hidden="true">💜</div>
<h1 class="title">What are you looking for?</h1>
<p class="sub">Be honest — you can always change this in settings.</p>
<div class="intents" id="intents" role="radiogroup" aria-label="Intentions"></div>
</section>
<!-- FINISH -->
<section class="step finish" data-step="5" aria-label="Welcome" hidden>
<div class="confetti" id="confetti" aria-hidden="true"></div>
<div class="spark-burst" aria-hidden="true">✷</div>
<h1 class="title big">You're all set, <span id="finishName">friend</span>!</h1>
<p class="sub">Your profile is live. Time to find your spark.</p>
<ul class="summary" id="summary"></ul>
<button class="btn btn-primary block" id="enterBtn" type="button">Start swiping</button>
</section>
</form>
</div>
<!-- Sticky footer CTA -->
<footer class="footer" id="footer">
<button class="btn btn-primary block" id="nextBtn" type="button">Continue</button>
<p class="legal">By continuing you agree to Spark's <a href="#" onclick="return false">Terms</a> & <a href="#" onclick="return false">Privacy</a>.</p>
</footer>
</div>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A complete mobile signup flow for a playful dating app, rendered inside a centered phone frame. The journey runs across five steps — phone verification, name and age, photo grid, interest tags, and dating intentions — each gated by inline validation so users cannot advance until the current step is valid. A row of progress dots at the top fills in as you move forward and stays tappable to jump back to any completed step.
Every control is genuinely interactive. The phone step formats digits as you type and reveals a mock verification field; the photo grid lets you add and remove gradient photo tiles up to six; interest chips toggle with a minimum-of-three rule; and the intention cards behave like a single-select radio group. Back and Next buttons preserve all state, and the final review is summarized before the confetti-filled welcome screen appears.
The layout is fully responsive down to 360px, uses the coral-to-violet gradient accent throughout, and keeps focus states, aria attributes, and keyboard operability intact. A small toast helper surfaces validation hints and confirmations without blocking the flow.