Dating — Discovery Grid
A playful mobile-app discovery screen where singles browse a responsive grid of dating profiles. Each card pairs a gradient photo with name, age and city, an animated online dot, and a heart like-button that pops into view on hover or focus. A sticky filter bar tunes distance and age range with live range sliders and quick vibe chips, while a shuffle control and load-more button keep the feed feeling infinite and fresh.
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 44px rgba(124, 58, 237, 0.16);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
font-family: Inter, system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
background:
radial-gradient(900px 500px at 12% -8%, rgba(255, 94, 108, 0.18), transparent 60%),
radial-gradient(900px 520px at 108% 12%, rgba(139, 92, 246, 0.18), transparent 60%),
var(--bg);
display: flex;
justify-content: center;
padding: 26px 14px;
}
/* Phone frame */
.phone {
width: 100%;
max-width: 430px;
height: min(880px, calc(100vh - 52px));
background: var(--surface);
border-radius: 38px;
box-shadow: var(--shadow), 0 0 0 10px #fff, 0 0 0 12px rgba(42, 26, 46, 0.06);
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
/* Top bar */
.topbar {
flex: 0 0 auto;
background: var(--surface);
border-bottom: 1px solid var(--line);
padding: 16px 16px 12px;
z-index: 5;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
}
.logo {
width: 38px;
height: 38px;
display: grid;
place-items: center;
border-radius: 12px;
background: var(--grad);
color: #fff;
font-size: 20px;
box-shadow: 0 8px 18px rgba(230, 57, 80, 0.32);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-text strong {
font-family: Fredoka, Inter, sans-serif;
font-size: 20px;
font-weight: 700;
letter-spacing: 0.2px;
}
.result-count { font-size: 12px; color: var(--muted); }
.liked-pill {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid var(--line);
background: #fff;
color: var(--coral-d);
font-weight: 700;
font-size: 14px;
padding: 7px 13px;
border-radius: 999px;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
.liked-pill:hover { transform: translateY(-1px); box-shadow: 0 8px 16px rgba(230, 57, 80, 0.18); }
.liked-pill:active { transform: translateY(0) scale(0.97); }
.liked-pill.bump { animation: bump 0.4s ease; }
.heart-mini { font-size: 15px; }
@keyframes bump {
0% { transform: scale(1); }
40% { transform: scale(1.22); }
100% { transform: scale(1); }
}
/* Filters */
.filters { margin-top: 14px; }
.slider-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.slider { display: flex; flex-direction: column; gap: 6px; }
.slider-head {
display: flex;
justify-content: space-between;
font-size: 12px;
font-weight: 600;
color: var(--ink-2);
}
.slider-head output { color: var(--violet-d); font-weight: 700; }
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 6px;
border-radius: 999px;
background: linear-gradient(90deg, var(--coral), var(--violet));
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #fff;
border: 3px solid var(--coral);
box-shadow: 0 3px 8px rgba(230, 57, 80, 0.4);
transition: transform 0.12s ease, border-color 0.12s ease;
}
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #fff;
border: 3px solid var(--coral);
box-shadow: 0 3px 8px rgba(230, 57, 80, 0.4);
}
input[type="range"]:hover::-webkit-slider-thumb { transform: scale(1.12); }
input[type="range"]:focus-visible::-webkit-slider-thumb { border-color: var(--violet-d); }
input[type="range"]:focus-visible { box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.25); }
.chips {
display: flex;
gap: 8px;
margin-top: 14px;
overflow-x: auto;
padding-bottom: 2px;
scrollbar-width: none;
}
.chips::-webkit-scrollbar { display: none; }
.chip {
flex: 0 0 auto;
border: 1px solid var(--line);
background: #fff;
color: var(--ink-2);
font-size: 13px;
font-weight: 600;
padding: 7px 14px;
border-radius: 999px;
cursor: pointer;
white-space: nowrap;
transition: transform 0.14s ease, background 0.14s ease, color 0.14s ease, border-color 0.14s ease;
}
.chip:hover { transform: translateY(-1px); border-color: var(--pink); }
.chip:active { transform: scale(0.96); }
.chip:focus-visible { outline: 3px solid rgba(139, 92, 246, 0.35); outline-offset: 2px; }
.chip.is-on {
background: var(--grad);
color: #fff;
border-color: transparent;
box-shadow: 0 8px 16px rgba(230, 57, 80, 0.26);
}
.chip.shuffle { color: var(--violet-d); }
/* Feed / grid */
.feed {
flex: 1 1 auto;
overflow-y: auto;
padding: 16px 16px 26px;
-webkit-overflow-scrolling: touch;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
/* Card */
.card {
position: relative;
border-radius: var(--r-md);
overflow: hidden;
aspect-ratio: 3 / 4;
background: var(--grad);
box-shadow: 0 10px 24px rgba(42, 26, 46, 0.14);
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
animation: cardIn 0.4s ease both;
color: #fff;
}
.card:hover, .card:focus-within {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(124, 58, 237, 0.28);
}
.card:focus-visible { outline: 3px solid var(--violet); outline-offset: 3px; }
@keyframes cardIn {
from { opacity: 0; transform: translateY(14px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.card .photo {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
}
.card .scrim {
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(20, 8, 24, 0.78) 0%, rgba(20, 8, 24, 0.12) 42%, transparent 70%);
}
.status {
position: absolute;
top: 10px;
left: 10px;
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 700;
padding: 4px 9px;
border-radius: 999px;
background: rgba(20, 8, 24, 0.42);
backdrop-filter: blur(6px);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #33d17a;
box-shadow: 0 0 0 0 rgba(51, 209, 122, 0.7);
animation: pulse 2s infinite;
}
.status.off .dot { background: #c9b8c4; animation: none; box-shadow: none; }
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(51, 209, 122, 0.6); }
70% { box-shadow: 0 0 0 7px rgba(51, 209, 122, 0); }
100% { box-shadow: 0 0 0 0 rgba(51, 209, 122, 0); }
}
.verified {
position: absolute;
top: 10px;
right: 10px;
width: 22px;
height: 22px;
display: grid;
place-items: center;
border-radius: 50%;
background: rgba(139, 92, 246, 0.92);
color: #fff;
font-size: 12px;
font-weight: 800;
}
.meta {
position: absolute;
left: 12px;
right: 12px;
bottom: 12px;
}
.name-row {
display: flex;
align-items: baseline;
gap: 6px;
}
.name-row .name { font-weight: 700; font-size: 16px; letter-spacing: 0.2px; }
.name-row .age { font-size: 15px; font-weight: 500; opacity: 0.92; }
.place {
font-size: 12px;
opacity: 0.9;
margin-top: 2px;
display: flex;
align-items: center;
gap: 4px;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 8px;
}
.tag {
font-size: 10.5px;
font-weight: 600;
padding: 3px 8px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.22);
backdrop-filter: blur(4px);
}
/* Like button */
.like {
position: absolute;
right: 10px;
bottom: 76px;
width: 44px;
height: 44px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, 0.94);
color: var(--coral-d);
font-size: 20px;
cursor: pointer;
display: grid;
place-items: center;
box-shadow: 0 8px 18px rgba(42, 26, 46, 0.3);
opacity: 0;
transform: scale(0.5) translateY(6px);
transition: opacity 0.2s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.2s ease;
}
.card:hover .like,
.card:focus-within .like { opacity: 1; transform: scale(1) translateY(0); }
.like:hover { background: #fff; transform: scale(1.12); }
.like:active { transform: scale(0.9); }
.like:focus-visible { outline: 3px solid var(--violet); outline-offset: 2px; opacity: 1; transform: scale(1); }
.like.liked {
background: var(--grad);
color: #fff;
opacity: 1;
transform: scale(1);
animation: pop 0.42s ease;
}
@keyframes pop {
0% { transform: scale(1); }
45% { transform: scale(1.35); }
100% { transform: scale(1); }
}
/* floating burst */
.burst {
position: absolute;
font-size: 18px;
pointer-events: none;
animation: floatUp 0.9s ease forwards;
z-index: 3;
}
@keyframes floatUp {
0% { opacity: 1; transform: translateY(0) scale(0.6); }
100% { opacity: 0; transform: translateY(-46px) scale(1.3); }
}
.empty {
text-align: center;
color: var(--muted);
font-weight: 600;
padding: 40px 10px;
}
.load-more {
display: block;
width: 100%;
margin-top: 18px;
padding: 14px;
border: none;
border-radius: 999px;
background: var(--grad);
color: #fff;
font-size: 15px;
font-weight: 700;
font-family: inherit;
cursor: pointer;
box-shadow: 0 12px 26px rgba(230, 57, 80, 0.3);
transition: transform 0.15s ease, box-shadow 0.15s ease, filter 0.15s ease;
}
.load-more:hover { transform: translateY(-2px); filter: brightness(1.04); }
.load-more:active { transform: translateY(0) scale(0.98); }
.load-more:focus-visible { outline: 3px solid var(--violet-d); outline-offset: 3px; }
.load-more.loading { pointer-events: none; opacity: 0.75; }
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 40px;
transform: translate(-50%, 24px);
background: var(--ink);
color: #fff;
font-weight: 600;
font-size: 14px;
padding: 12px 20px;
border-radius: 999px;
box-shadow: 0 14px 30px rgba(42, 26, 46, 0.35);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 40;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
@media (max-width: 520px) {
body { padding: 0; align-items: stretch; }
.phone {
max-width: 100%;
height: 100vh;
border-radius: 0;
box-shadow: none;
}
.slider-row { grid-template-columns: 1fr; }
.grid { gap: 12px; }
}
@media (max-width: 360px) {
.grid { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
// ---- Fictional but believable data -------------------------------------
var PHOTOS = [
"https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=500&q=80",
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=500&q=80",
"https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=500&q=80",
"https://images.unsplash.com/photo-1531123897727-8f129e1688ce?w=500&q=80",
"https://images.unsplash.com/photo-1517841905240-472988babdf9?w=500&q=80",
"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=500&q=80",
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=500&q=80",
"https://images.unsplash.com/photo-1508214751196-bcfd4ca60f91?w=500&q=80",
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=500&q=80",
"https://images.unsplash.com/photo-1502823403499-6ccfcf4fb453?w=500&q=80"
];
var GRADS = [
"linear-gradient(135deg,#ff5e6c,#8b5cf6)",
"linear-gradient(135deg,#ff8fb1,#7c3aed)",
"linear-gradient(135deg,#fbb040,#ff5e6c)",
"linear-gradient(135deg,#8b5cf6,#38bdf8)",
"linear-gradient(135deg,#ff5e6c,#ff8fb1)"
];
var FIRST = ["Mara", "Leo", "Priya", "Diego", "Aya", "Noah", "Sofia", "Ravi", "Elena", "Kai",
"Nina", "Theo", "Luna", "Marco", "Zoe", "Ivan", "Rosa", "Sam", "Yuki", "Ben"];
var CITIES = ["Downtown", "Riverside", "Old Town", "Harbor", "Midtown", "The Heights", "Seaside", "Parkview"];
var INTERESTS = ["Coffee", "Hiking", "Vinyl", "Yoga", "Ramen", "Film", "Climbing", "Painting",
"Dogs", "Travel", "Board games", "Live music", "Baking", "Cycling"];
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
var uid = 0;
function makeProfile() {
var tags = [];
var pool = INTERESTS.slice();
for (var i = 0; i < 2; i++) tags.push(pool.splice(rand(0, pool.length - 1), 1)[0]);
return {
id: ++uid,
name: pick(FIRST),
age: rand(21, 39),
city: pick(CITIES),
dist: rand(1, 78),
online: Math.random() > 0.42,
isNew: Math.random() > 0.7,
verified: Math.random() > 0.55,
photo: pick(PHOTOS),
grad: pick(GRADS),
tags: tags,
liked: false
};
}
var profiles = [];
function seed(n) { for (var i = 0; i < n; i++) profiles.push(makeProfile()); }
seed(14);
// ---- Elements ----------------------------------------------------------
var grid = document.getElementById("grid");
var empty = document.getElementById("empty");
var distance = document.getElementById("distance");
var age = document.getElementById("age");
var distanceOut = document.getElementById("distanceOut");
var ageOut = document.getElementById("ageOut");
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip[data-vibe]"));
var shuffleBtn = document.getElementById("shuffleBtn");
var loadMore = document.getElementById("loadMore");
var resultCount = document.getElementById("resultCount");
var likedCount = document.getElementById("likedCount");
var likedPill = document.getElementById("likedPill");
var toastEl = document.getElementById("toast");
var vibe = "all";
var likes = 0;
// ---- Toast -------------------------------------------------------------
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2000);
}
// ---- Filtering ---------------------------------------------------------
function visible() {
var maxD = +distance.value;
var maxA = +age.value;
return profiles.filter(function (p) {
if (p.dist > maxD) return false;
if (p.age > maxA) return false;
if (vibe === "online" && !p.online) return false;
if (vibe === "new" && !p.isNew) return false;
if (vibe === "verified" && !p.verified) return false;
return true;
});
}
// ---- Render ------------------------------------------------------------
function cardHTML(p) {
var status = p.online
? '<span class="status"><span class="dot"></span>Online</span>'
: '<span class="status off"><span class="dot"></span>Away</span>';
var badge = p.verified ? '<span class="verified" title="Verified">✓</span>' : "";
var tags = p.tags.map(function (t) { return '<span class="tag">' + t + "</span>"; }).join("");
var likedCls = p.liked ? " liked" : "";
var pressed = p.liked ? "true" : "false";
var label = (p.liked ? "Unlike " : "Like ") + p.name;
return (
'<article class="card" tabindex="0" data-id="' + p.id + '" ' +
'aria-label="' + p.name + ", " + p.age + ", " + p.city + ", " + p.dist + ' km away">' +
'<div class="photo" style="background-image:' + p.grad + ";background-image:url(" + p.photo + ')"></div>' +
'<div class="scrim"></div>' +
status + badge +
'<button class="like' + likedCls + '" type="button" data-like="' + p.id + '" ' +
'aria-pressed="' + pressed + '" aria-label="' + label + '">♥</button>' +
'<div class="meta">' +
'<div class="name-row"><span class="name">' + p.name + '</span>' +
'<span class="age">' + p.age + "</span></div>" +
'<div class="place">📍 ' + p.city + " · " + p.dist + ' km</div>' +
'<div class="tags">' + tags + "</div>" +
"</div>" +
"</article>"
);
}
function render() {
var list = visible();
grid.innerHTML = list.map(cardHTML).join("");
empty.hidden = list.length !== 0;
var onlineCount = list.filter(function (p) { return p.online; }).length;
resultCount.textContent = list.length + " people · " + onlineCount + " online";
}
// ---- Like handling -----------------------------------------------------
function toggleLike(id, btn) {
var p = profiles.filter(function (x) { return x.id === id; })[0];
if (!p) return;
p.liked = !p.liked;
btn.classList.toggle("liked", p.liked);
btn.setAttribute("aria-pressed", p.liked ? "true" : "false");
btn.setAttribute("aria-label", (p.liked ? "Unlike " : "Like ") + p.name);
if (p.liked) {
likes++;
spawnBurst(btn);
likedPill.classList.remove("bump");
void likedPill.offsetWidth;
likedPill.classList.add("bump");
toast("You liked " + p.name + " 💖");
} else {
likes = Math.max(0, likes - 1);
toast("Removed " + p.name);
}
likedCount.textContent = likes;
}
function spawnBurst(btn) {
var card = btn.closest(".card");
if (!card) return;
var b = document.createElement("span");
b.className = "burst";
b.textContent = pick(["💖", "💘", "✨", "💕"]);
b.style.right = "16px";
b.style.bottom = "84px";
card.appendChild(b);
setTimeout(function () { if (b.parentNode) b.parentNode.removeChild(b); }, 900);
}
// ---- Events ------------------------------------------------------------
grid.addEventListener("click", function (e) {
var likeBtn = e.target.closest("[data-like]");
if (likeBtn) {
e.stopPropagation();
toggleLike(+likeBtn.getAttribute("data-like"), likeBtn);
return;
}
var card = e.target.closest(".card");
if (card) {
var name = card.querySelector(".name").textContent;
toast("Opening " + name + "'s profile…");
}
});
// keyboard: Enter/Space on a focused card likes the profile
grid.addEventListener("keydown", function (e) {
var card = e.target.closest(".card");
if (!card) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
var btn = card.querySelector("[data-like]");
if (btn) toggleLike(+btn.getAttribute("data-like"), btn);
}
});
distance.addEventListener("input", function () {
distanceOut.textContent = distance.value + " km";
render();
});
age.addEventListener("input", function () {
ageOut.textContent = age.value;
render();
});
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
chips.forEach(function (c) {
c.classList.remove("is-on");
c.setAttribute("aria-pressed", "false");
});
chip.classList.add("is-on");
chip.setAttribute("aria-pressed", "true");
vibe = chip.getAttribute("data-vibe");
render();
});
});
shuffleBtn.addEventListener("click", function () {
for (var i = profiles.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var t = profiles[i]; profiles[i] = profiles[j]; profiles[j] = t;
}
render();
toast("Fresh deck shuffled 🔀");
});
loadMore.addEventListener("click", function () {
loadMore.classList.add("loading");
loadMore.textContent = "Finding people…";
setTimeout(function () {
seed(8);
render();
loadMore.classList.remove("loading");
loadMore.textContent = "Load more people";
toast("8 new people nearby ✨");
var feed = document.getElementById("feed");
feed.scrollTo({ top: feed.scrollHeight, behavior: "smooth" });
}, 700);
});
likedPill.addEventListener("click", function () {
toast(likes === 0 ? "No likes yet — tap a heart 💗" : "You have " + likes + " like" + (likes === 1 ? "" : "s"));
});
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Spark — Discovery</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=Fredoka:wght@500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="phone" role="application" aria-label="Spark dating discovery">
<!-- Sticky top: brand + filters -->
<header class="topbar">
<div class="brand">
<span class="logo" aria-hidden="true">✦</span>
<div class="brand-text">
<strong>Spark</strong>
<span id="resultCount" class="result-count" aria-live="polite">24 people nearby</span>
</div>
<button class="liked-pill" id="likedPill" type="button" aria-label="Liked profiles">
<span class="heart-mini" aria-hidden="true">♥</span>
<span id="likedCount">0</span>
</button>
</div>
<div class="filters" aria-label="Filters">
<div class="slider-row">
<label class="slider" for="distance">
<span class="slider-head">
<span>Max distance</span>
<output id="distanceOut" for="distance">25 km</output>
</span>
<input id="distance" type="range" min="2" max="80" value="25" step="1"
aria-label="Maximum distance in kilometres" />
</label>
<label class="slider" for="age">
<span class="slider-head">
<span>Age up to</span>
<output id="ageOut" for="age">34</output>
</span>
<input id="age" type="range" min="19" max="55" value="34" step="1"
aria-label="Maximum age" />
</label>
</div>
<div class="chips" role="group" aria-label="Quick filters">
<button class="chip is-on" type="button" data-vibe="all" aria-pressed="true">All</button>
<button class="chip" type="button" data-vibe="online" aria-pressed="false">🟢 Online</button>
<button class="chip" type="button" data-vibe="new" aria-pressed="false">✨ New here</button>
<button class="chip" type="button" data-vibe="verified" aria-pressed="false">✓ Verified</button>
<button class="chip shuffle" type="button" id="shuffleBtn" aria-label="Shuffle deck">🔀 Shuffle</button>
</div>
</div>
</header>
<!-- Scrollable grid -->
<main class="feed" id="feed" aria-label="Profiles">
<section class="grid" id="grid" aria-live="polite"></section>
<p class="empty" id="empty" hidden>No one matches those filters yet. Widen your range 💫</p>
<button class="load-more" id="loadMore" type="button">Load more people</button>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>The Discovery Grid is the browse screen of a dating app, wrapped in a ~430px phone frame so it reads like a real handset. A sticky header holds a filter bar with two live range sliders (max distance and age range) plus a row of vibe chips (Nearby, New here, Verified, Online). Below it, profiles flow into a two-column card grid — each card is a rounded gradient portrait with an online status dot, a name and age, city and distance, and interest badges. Hovering or focusing a card reveals a floating heart button that pops with a spring animation.
Liking a profile animates the heart, floats a small burst, fires a toast, and moves that card to a Liked count in the header. The filter controls re-run instantly: dragging the distance or age sliders, or toggling a vibe chip, filters the visible cards with a soft fade and updates the live result count. A Shuffle button reorders the deck, and a Load more button appends a fresh batch of profiles for that never-ending, keep-swiping feel.
Everything is vanilla — no frameworks, no build step. Cards are keyboard reachable, the like buttons are
real <button>s with aria-pressed, the sliders announce their values, and a @media (max-width:520px)
block keeps the grid comfortable down to 360px. Fictional-but-believable profiles and Unsplash portraits
make it feel populated without hotlinking anything unpredictable.