Dating — Profile Card
A playful dating profile card with a swipeable photo carousel, tap-to-advance dots, verified badge, live online status, distance and last-active meta, an expandable bio, and colorful interest chips — plus like, pass and super-like actions with buttery micro-interactions and toast feedback, all wrapped in a mobile phone frame.
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 24px 60px -18px rgba(124, 58, 237, 0.35);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
min-height: 100vh;
font-family: Inter, system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1000px 500px at 15% -10%, rgba(255, 143, 177, 0.35), transparent 60%),
radial-gradient(900px 500px at 110% 20%, rgba(139, 92, 246, 0.22), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.stage {
min-height: 100vh;
display: grid;
place-items: center;
padding: 32px 16px;
}
/* Phone frame */
.phone {
width: 100%;
max-width: 430px;
background: var(--surface);
border-radius: 40px;
padding: 12px;
box-shadow: var(--shadow), 0 0 0 1px var(--line);
overflow: hidden;
}
.phone__bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px 14px;
}
.brand {
font-weight: 800;
font-size: 20px;
letter-spacing: -0.02em;
background: var(--grad);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.brand__dot { color: var(--coral); -webkit-text-fill-color: var(--coral); }
.phone__count {
font-size: 12.5px;
font-weight: 600;
color: var(--violet-d);
background: rgba(139, 92, 246, 0.10);
padding: 6px 12px;
border-radius: 999px;
}
/* Card */
.card {
border-radius: var(--r-lg);
overflow: hidden;
background: var(--surface);
box-shadow: 0 10px 30px -12px rgba(42, 26, 46, 0.25);
transition: transform .45s cubic-bezier(.2, .8, .2, 1), opacity .45s ease;
}
.card.is-leaving-like { transform: translateX(120%) rotate(12deg); opacity: 0; }
.card.is-leaving-pass { transform: translateX(-120%) rotate(-12deg); opacity: 0; }
.card.is-leaving-super { transform: translateY(-120%) scale(.9); opacity: 0; }
.card.is-entering { animation: pop .5s cubic-bezier(.2, .9, .25, 1.2); }
@keyframes pop {
from { transform: scale(.94) translateY(10px); opacity: 0; }
to { transform: none; opacity: 1; }
}
/* Carousel */
.carousel {
position: relative;
aspect-ratio: 4 / 5;
outline: none;
overflow: hidden;
}
.carousel:focus-visible { box-shadow: inset 0 0 0 3px var(--violet); }
.carousel__track {
display: flex;
height: 100%;
transition: transform .4s cubic-bezier(.3, .8, .25, 1);
}
.slide {
min-width: 100%;
height: 100%;
background-size: cover;
background-position: center;
}
.carousel__nav {
position: absolute;
top: 0;
bottom: 0;
width: 42%;
border: 0;
background: transparent;
cursor: pointer;
z-index: 2;
}
.carousel__nav--prev { left: 0; }
.carousel__nav--next { right: 0; }
.carousel__nav:focus-visible {
outline: 3px solid var(--white);
outline-offset: -6px;
border-radius: 20px;
}
.carousel__dots {
position: absolute;
top: 12px;
left: 12px;
right: 12px;
display: flex;
gap: 6px;
z-index: 3;
}
.dot {
flex: 1;
height: 4px;
border: 0;
padding: 0;
border-radius: 999px;
background: rgba(255, 255, 255, 0.4);
cursor: pointer;
overflow: hidden;
transition: background .2s ease;
}
.dot::after {
content: "";
display: block;
height: 100%;
width: 0;
background: var(--white);
border-radius: 999px;
transition: width .3s ease;
}
.dot.is-active::after { width: 100%; }
.dot:focus-visible { outline: 2px solid var(--white); outline-offset: 2px; }
.carousel__overlay {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 3;
padding: 60px 18px 18px;
background: linear-gradient(to top, rgba(20, 8, 22, 0.85), rgba(20, 8, 22, 0.15) 60%, transparent);
color: var(--white);
pointer-events: none;
}
.carousel__overlay .status,
.carousel__overlay .verified { pointer-events: auto; }
.status {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12px;
font-weight: 600;
background: rgba(255, 255, 255, 0.16);
backdrop-filter: blur(6px);
padding: 5px 11px 5px 9px;
border-radius: 999px;
margin-bottom: 10px;
}
.status__dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: #34d399;
box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.7);
animation: pulse 2s infinite;
}
.status.is-away .status__dot { background: #fbbf24; animation: none; }
.status.is-offline .status__dot { background: #9ca3af; animation: none; }
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.6); }
70% { box-shadow: 0 0 0 8px rgba(52, 211, 153, 0); }
100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); }
}
.identity__name {
margin: 0;
font-size: 27px;
font-weight: 800;
letter-spacing: -0.02em;
display: flex;
align-items: center;
gap: 4px;
font-family: Fraunces, Inter, serif;
}
.identity__age { font-weight: 500; opacity: .92; }
.verified {
display: inline-grid;
place-items: center;
width: 24px;
height: 24px;
margin-left: 4px;
border-radius: 50%;
background: var(--grad);
color: var(--white);
flex: none;
}
.verified svg { width: 18px; height: 18px; }
.identity__meta {
margin: 8px 0 0;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.pill {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12.5px;
font-weight: 600;
padding: 5px 11px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.18);
backdrop-filter: blur(6px);
}
.pill svg { width: 14px; height: 14px; }
/* Body */
.card__body { padding: 16px 18px 6px; }
.bio {
margin: 0;
color: var(--ink-2);
font-size: 14.5px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.bio.is-open { -webkit-line-clamp: unset; }
.bio__toggle {
margin-top: 4px;
border: 0;
background: none;
padding: 2px 0;
font: inherit;
font-size: 13px;
font-weight: 700;
color: var(--violet-d);
cursor: pointer;
border-radius: 6px;
}
.bio__toggle:focus-visible { outline: 2px solid var(--violet); outline-offset: 3px; }
.chips {
list-style: none;
margin: 14px 0 4px;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12.5px;
font-weight: 600;
color: var(--ink);
padding: 7px 12px;
border-radius: 999px;
background: var(--bg);
border: 1px solid var(--line);
cursor: default;
transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.chip:hover {
transform: translateY(-2px);
border-color: transparent;
box-shadow: 0 6px 14px -6px rgba(255, 94, 108, 0.5);
}
.chip__emoji { font-size: 14px; }
.chip--shared {
background: rgba(255, 94, 108, 0.10);
border-color: rgba(255, 94, 108, 0.28);
color: var(--coral-d);
}
/* Actions */
.actions {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
padding: 14px 18px 20px;
}
.act {
display: grid;
place-items: center;
border: 0;
cursor: pointer;
border-radius: 50%;
background: var(--surface);
box-shadow: 0 8px 20px -8px rgba(42, 26, 46, 0.35), inset 0 0 0 1.5px var(--line);
transition: transform .18s ease, box-shadow .18s ease, color .18s ease;
}
.act svg { width: 26px; height: 26px; }
.act--pass { width: 56px; height: 56px; color: var(--muted); }
.act--super { width: 50px; height: 50px; color: var(--violet); }
.act--like { width: 64px; height: 64px; color: var(--coral); }
.act:hover { transform: translateY(-3px) scale(1.05); }
.act:active { transform: scale(.92); }
.act--pass:hover { color: var(--ink); box-shadow: 0 10px 22px -8px rgba(42, 26, 46, 0.4), inset 0 0 0 1.5px var(--ink); }
.act--super:hover { color: var(--white); background: var(--violet); box-shadow: 0 12px 24px -8px rgba(139, 92, 246, 0.6); }
.act--like:hover { color: var(--white); background: var(--grad); box-shadow: 0 14px 28px -8px rgba(255, 94, 108, 0.6); }
.act:focus-visible { outline: 3px solid var(--violet); outline-offset: 3px; }
.act.is-tapped { animation: bump .4s ease; }
@keyframes bump {
30% { transform: scale(1.25); }
60% { transform: scale(.9); }
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 24px);
background: var(--ink);
color: var(--white);
font-size: 14px;
font-weight: 600;
padding: 12px 20px;
border-radius: 999px;
box-shadow: 0 14px 30px -10px rgba(42, 26, 46, 0.6);
opacity: 0;
pointer-events: none;
transition: opacity .3s ease, transform .3s cubic-bezier(.2, .9, .25, 1.2);
z-index: 50;
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
.toast--like { background: linear-gradient(135deg, var(--coral-d), var(--violet-d)); }
.toast--super { background: var(--violet-d); }
@media (max-width: 520px) {
.stage { padding: 14px 10px; }
.phone { border-radius: 28px; padding: 8px; }
.identity__name { font-size: 24px; }
.act--pass { width: 52px; height: 52px; }
.act--like { width: 58px; height: 58px; }
.act--super { width: 46px; height: 46px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}(function () {
"use strict";
// --- Data: a small deck of clearly-fictional profiles ---------------------
var PROFILES = [
{
name: "Mara", age: 27, job: "Ceramicist", dist: "3 km away",
status: "online", statusText: "Online now", verified: true,
bio: "Weekend potter, weekday product designer. I make ugly mugs and excellent playlists. Looking for someone to split dumplings with and lose to at Mario Kart. Bonus points if you have strong opinions about the correct way to make coffee.",
photos: ["#ff5e6c,#8b5cf6", "#8b5cf6,#7c3aed", "#ff8fb1,#ff5e6c"],
interests: [
{ e: "\u{1F3FA}", t: "Pottery", shared: false },
{ e: "☕", t: "Coffee", shared: true },
{ e: "\u{1F3AE}", t: "Mario Kart", shared: false },
{ e: "\u{1F35C}", t: "Dumplings", shared: true },
{ e: "\u{1F3B5}", t: "Playlists", shared: false }
]
},
{
name: "Dev", age: 31, job: "Trail runner", dist: "6 km away",
status: "away", statusText: "Active 12m ago", verified: true,
bio: "Software person who escapes to mountains on weekends. I will absolutely make you try my sourdough. Fluent in sarcasm and dog. Two cats named after Star Wars characters. Let's get lost on a trail and grab tacos after.",
photos: ["#7c3aed,#ff8fb1", "#ff5e6c,#e63950", "#8b5cf6,#ff5e6c"],
interests: [
{ e: "\u{1F3C3}", t: "Running", shared: false },
{ e: "\u{1F35E}", t: "Sourdough", shared: false },
{ e: "\u{1F32E}", t: "Tacos", shared: true },
{ e: "\u{1F408}", t: "Cats", shared: false },
{ e: "⛰️", t: "Hiking", shared: true }
]
},
{
name: "Priya", age: 25, job: "Jazz pianist", dist: "1 km away",
status: "online", statusText: "Online now", verified: false,
bio: "I play piano in smoky bars and read too many novels at once. Seeking a partner in crime for late-night bookstores and vinyl hunting. I promise I'm funnier in person and I will steal fries off your plate.",
photos: ["#ff8fb1,#8b5cf6", "#e63950,#ff5e6c", "#8b5cf6,#7c3aed"],
interests: [
{ e: "\u{1F3B9}", t: "Jazz", shared: false },
{ e: "\u{1F4DA}", t: "Novels", shared: true },
{ e: "\u{1F4C0}", t: "Vinyl", shared: false },
{ e: "\u{1F35F}", t: "Fries", shared: true }
]
}
];
// --- Elements -------------------------------------------------------------
var card = document.getElementById("card");
var track = document.getElementById("track");
var dotsWrap = document.getElementById("dots");
var chipsWrap = document.getElementById("chips");
var prevBtn = document.getElementById("prevBtn");
var nextBtn = document.getElementById("nextBtn");
var carousel = document.getElementById("carousel");
var bio = document.getElementById("bio");
var bioToggle = document.getElementById("bioToggle");
var toastEl = document.getElementById("toast");
var matchCount = document.getElementById("matchCount");
var statusEl = document.getElementById("status");
var elName = document.getElementById("pName");
var elAge = document.getElementById("pAge");
var elJob = document.getElementById("pJob");
var elDist = document.getElementById("pDist");
var elStatusText = document.getElementById("statusText");
var elVerified = document.getElementById("verified");
var profileIndex = 0;
var photoIndex = 0;
var likes = 0;
var toastTimer = null;
// --- Toast helper ---------------------------------------------------------
function toast(msg, variant) {
toastEl.textContent = msg;
toastEl.className = "toast is-show" + (variant ? " toast--" + variant : "");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 1900);
}
// --- Render a profile -----------------------------------------------------
function render(profile) {
photoIndex = 0;
// Photos as gradient slides
track.innerHTML = "";
dotsWrap.innerHTML = "";
profile.photos.forEach(function (colors, i) {
var pair = colors.split(",");
var slide = document.createElement("div");
slide.className = "slide";
slide.style.background = "linear-gradient(135deg, " + pair[0] + ", " + pair[1] + ")";
track.appendChild(slide);
var dot = document.createElement("button");
dot.type = "button";
dot.className = "dot" + (i === 0 ? " is-active" : "");
dot.setAttribute("role", "tab");
dot.setAttribute("aria-label", "Photo " + (i + 1) + " of " + profile.photos.length);
dot.addEventListener("click", function () { goTo(i); });
dotsWrap.appendChild(dot);
});
// Identity
elName.textContent = profile.name;
elAge.textContent = ", " + profile.age;
elJob.textContent = profile.job;
elDist.textContent = profile.dist;
elStatusText.textContent = profile.statusText;
elVerified.style.display = profile.verified ? "" : "none";
// Status style
statusEl.className = "status" + (profile.status === "away" ? " is-away" : profile.status === "offline" ? " is-offline" : "");
// Bio
bio.textContent = profile.bio;
bio.classList.remove("is-open");
bioToggle.textContent = "Read more";
bioToggle.setAttribute("aria-expanded", "false");
// Interests
chipsWrap.innerHTML = "";
profile.interests.forEach(function (it) {
var li = document.createElement("li");
li.className = "chip" + (it.shared ? " chip--shared" : "");
li.innerHTML = '<span class="chip__emoji" aria-hidden="true">' + it.e + "</span>" + it.t + (it.shared ? " • you too" : "");
chipsWrap.appendChild(li);
});
updateTrack();
}
// --- Carousel logic -------------------------------------------------------
function updateTrack() {
track.style.transform = "translateX(" + (-photoIndex * 100) + "%)";
var dots = dotsWrap.querySelectorAll(".dot");
dots.forEach(function (d, i) {
d.classList.toggle("is-active", i === photoIndex);
});
}
function goTo(i) {
var total = PROFILES[profileIndex].photos.length;
photoIndex = (i + total) % total;
updateTrack();
}
function nextPhoto() { goTo(photoIndex + 1); }
function prevPhoto() { goTo(photoIndex - 1); }
prevBtn.addEventListener("click", prevPhoto);
nextBtn.addEventListener("click", nextPhoto);
carousel.addEventListener("keydown", function (e) {
if (e.key === "ArrowRight") { e.preventDefault(); nextPhoto(); }
else if (e.key === "ArrowLeft") { e.preventDefault(); prevPhoto(); }
});
// Touch swipe
var touchX = null;
carousel.addEventListener("touchstart", function (e) { touchX = e.touches[0].clientX; }, { passive: true });
carousel.addEventListener("touchend", function (e) {
if (touchX === null) return;
var dx = e.changedTouches[0].clientX - touchX;
if (Math.abs(dx) > 40) { dx < 0 ? nextPhoto() : prevPhoto(); }
touchX = null;
});
// --- Bio expand -----------------------------------------------------------
bioToggle.addEventListener("click", function () {
var open = bio.classList.toggle("is-open");
bioToggle.textContent = open ? "Show less" : "Read more";
bioToggle.setAttribute("aria-expanded", String(open));
});
// --- Actions --------------------------------------------------------------
function bump(btn) {
btn.classList.remove("is-tapped");
void btn.offsetWidth;
btn.classList.add("is-tapped");
}
function advanceProfile(dir) {
var leaving = "is-leaving-" + dir;
card.classList.add(leaving);
setTimeout(function () {
profileIndex = (profileIndex + 1) % PROFILES.length;
render(PROFILES[profileIndex]);
card.classList.remove(leaving);
card.classList.add("is-entering");
setTimeout(function () { card.classList.remove("is-entering"); }, 500);
}, 380);
}
function updateCount() {
matchCount.textContent = likes + (likes === 1 ? " like sent" : " likes sent");
}
document.getElementById("likeBtn").addEventListener("click", function () {
bump(this);
likes++; updateCount();
toast("You liked " + PROFILES[profileIndex].name + " ❤️", "like");
advanceProfile("like");
});
document.getElementById("superBtn").addEventListener("click", function () {
bump(this);
likes++; updateCount();
toast("Super liked " + PROFILES[profileIndex].name + "! ⭐", "super");
advanceProfile("super");
});
document.getElementById("passBtn").addEventListener("click", function () {
bump(this);
toast("Passed — next up");
advanceProfile("pass");
});
// --- Init -----------------------------------------------------------------
render(PROFILES[0]);
updateCount();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Dating — Profile Card</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=Fraunces:opsz,[email protected],600;9..144,700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="stage">
<div class="phone" role="region" aria-label="Discover people">
<header class="phone__bar">
<span class="brand">spark<span class="brand__dot">•</span></span>
<span class="phone__count" id="matchCount" aria-live="polite">0 likes sent</span>
</header>
<article class="card" id="card" aria-roledescription="profile card">
<div class="carousel" id="carousel" tabindex="0" role="group" aria-label="Photos of Mara, use arrow keys to browse">
<div class="carousel__track" id="track"></div>
<div class="carousel__dots" id="dots" role="tablist" aria-label="Photo navigation"></div>
<button class="carousel__nav carousel__nav--prev" id="prevBtn" type="button" aria-label="Previous photo"></button>
<button class="carousel__nav carousel__nav--next" id="nextBtn" type="button" aria-label="Next photo"></button>
<div class="carousel__overlay">
<span class="status" id="status">
<span class="status__dot" aria-hidden="true"></span>
<span id="statusText">Online now</span>
</span>
<div class="identity">
<h1 class="identity__name">
<span id="pName">Mara</span><span class="identity__age" id="pAge">, 27</span>
<span class="verified" id="verified" title="Photo verified" aria-label="Verified profile">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 12.5l2 2 4-4.5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</h1>
<p class="identity__meta">
<span class="pill pill--dist"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-6.5-5.7-6.5-10A6.5 6.5 0 0 1 18.5 11c0 4.3-6.5 10-6.5 10z" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="11" r="2.3" fill="currentColor"/></svg><span id="pDist">3 km away</span></span>
<span class="pill pill--job" id="pJob">Ceramicist</span>
</p>
</div>
</div>
</div>
<div class="card__body">
<p class="bio" id="bio">Weekend potter, weekday product designer. I make ugly mugs and excellent playlists. Looking for someone to split dumplings with and lose to at Mario Kart. Bonus points if you have strong opinions about the correct way to make coffee.</p>
<button class="bio__toggle" id="bioToggle" type="button" aria-expanded="false">Read more</button>
<ul class="chips" id="chips" aria-label="Interests"></ul>
</div>
<div class="actions">
<button class="act act--pass" id="passBtn" type="button" aria-label="Pass">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/></svg>
</button>
<button class="act act--super" id="superBtn" type="button" aria-label="Super like">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3l2.6 5.6L20.5 9.4l-4.3 4.1 1 6L12 16.9 6.8 19.5l1-6L3.5 9.4l5.9-.8z" fill="currentColor"/></svg>
</button>
<button class="act act--like" id="likeBtn" type="button" aria-label="Like">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7-4.6-9.3-9.1C1 8.6 2.6 5 6 5c2 0 3.2 1.1 4 2.3C10.8 6.1 12 5 14 5c3.4 0 5 3.6 3.3 6.9C19 16.4 12 21 12 21z" fill="currentColor"/></svg>
</button>
</div>
</article>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
</main>
<script src="script.js"></script>
</body>
</html>A self-contained dating profile card built for a mobile-app feel. The card lives inside a ~430px phone frame and leads with a full-bleed photo carousel: tap the left or right half of the image to move between shots, or click the progress dots up top. Each profile shows the person’s name and age, a verified checkmark, a pulsing online-status dot, and a distance pill, with the gradient coral-to-violet theme carried through the badges and action buttons.
Below the photo sits an expandable bio that clamps to two lines until you hit “Read more”, followed by a row of interest chips that gently lift on hover. Three pill actions — pass, like and super-like — sit at the bottom; each fires a toast, bumps a match counter, and animates so the interaction feels alive.
The whole component is keyboard-usable (arrow keys move the carousel, buttons are focusable with visible focus rings) and responsive down to ~360px. Everything is vanilla JS with no dependencies, so it drops into any page as a starting point for a swipe deck or discovery feed.