Dating — Match Modal
A joyful It is a match celebration overlay for dating apps: two circular avatars glide together and lock behind a gradient heart badge while confetti and hearts burst across a blurred backdrop. Pill Send message and Keep swiping actions sit below a matched name, and a replay trigger re-runs the entrance so you can feel the dopamine hit again. Fully responsive, keyboard-friendly, and dependency-free vanilla JavaScript.
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.1);
--white: #fff;
--r-sm: 12px;
--r-md: 20px;
--r-lg: 28px;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
gap: 18px;
padding: 32px 16px;
font-family: Inter, system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1000px 600px at 15% -10%, #ffe1e7 0%, transparent 55%),
radial-gradient(900px 620px at 110% 10%, #ede4ff 0%, transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ===== Phone frame ===== */
.phone {
position: relative;
width: 430px;
max-width: 100%;
aspect-ratio: 430 / 780;
border-radius: 44px;
background: linear-gradient(180deg, #fff 0%, #fff5f6 100%);
box-shadow:
0 40px 80px -30px rgba(124, 58, 237, 0.4),
0 0 0 10px #1c1420,
0 0 0 12px rgba(255, 255, 255, 0.08);
overflow: hidden;
isolation: isolate;
}
.phone__notch {
position: absolute;
top: 12px;
left: 50%;
transform: translateX(-50%);
width: 130px;
height: 26px;
border-radius: 14px;
background: #1c1420;
z-index: 5;
}
/* ===== App top bar ===== */
.app-top {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
padding: 48px 22px 12px;
}
.app-top__logo {
font-family: "Baloo 2", Inter, sans-serif;
font-weight: 800;
font-size: 26px;
letter-spacing: -0.5px;
background: var(--grad);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.app-top__pill {
font-size: 13px;
font-weight: 600;
color: var(--violet-d);
background: #f0e9ff;
padding: 6px 14px;
border-radius: 999px;
}
/* ===== Deck / swipe card ===== */
.deck {
padding: 6px 22px 0;
}
.swipe-card {
position: relative;
border-radius: var(--r-lg);
background: var(--surface);
box-shadow: 0 18px 40px -22px rgba(42, 26, 46, 0.5);
overflow: hidden;
border: 1px solid var(--line);
}
.swipe-card__photo {
height: 300px;
background-size: cover;
background-position: center;
}
.swipe-card__photo--a {
background-image:
linear-gradient(180deg, rgba(0,0,0,0) 45%, rgba(42,26,46,0.55) 100%),
url("https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=800&q=70");
}
.swipe-card__meta {
padding: 14px 18px 20px;
}
.swipe-card__meta h2 {
margin: 0;
font-size: 22px;
font-weight: 800;
letter-spacing: -0.3px;
}
.swipe-card__meta p {
margin: 4px 0 12px;
font-size: 14px;
color: var(--ink-2);
}
.swipe-card__tags { display: flex; gap: 8px; flex-wrap: wrap; }
.tag {
font-size: 12px;
font-weight: 600;
color: var(--ink-2);
background: #f6eef6;
padding: 5px 11px;
border-radius: 999px;
}
.tag--new { color: var(--white); background: var(--grad); }
/* ===== Dock ===== */
.dock {
position: absolute;
bottom: 26px;
left: 0;
right: 0;
display: flex;
justify-content: center;
gap: 20px;
z-index: 2;
}
.dock__btn {
width: 58px;
height: 58px;
border-radius: 50%;
border: none;
font-size: 22px;
cursor: pointer;
background: var(--white);
box-shadow: 0 12px 24px -10px rgba(42, 26, 46, 0.45);
transition: transform 0.15s ease;
}
.dock__btn--nope { color: var(--muted); }
.dock__btn--star { color: var(--violet); font-size: 24px; }
.dock__btn--like {
width: 68px; height: 68px; color: var(--white);
background: var(--grad); font-size: 26px;
}
/* ===== Overlay ===== */
.overlay {
position: absolute;
inset: 0;
z-index: 10;
display: grid;
place-items: center;
padding: 20px;
background: rgba(42, 26, 46, 0.5);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
animation: overlayIn 0.35s ease both;
}
.overlay[hidden] { display: none; }
@keyframes overlayIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* ===== Match card ===== */
.match {
position: relative;
z-index: 2;
width: 100%;
max-width: 340px;
text-align: center;
color: var(--white);
}
.match__eyebrow {
margin: 0 0 6px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.4px;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.85);
animation: riseIn 0.5s 0.15s ease both;
}
.match__title {
margin: 0 0 26px;
font-family: "Baloo 2", Inter, sans-serif;
font-weight: 800;
font-size: 42px;
line-height: 1.05;
letter-spacing: -1px;
background: linear-gradient(120deg, #fff 0%, #ffe3ea 55%, #e9dcff 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(0 4px 16px rgba(255, 94, 108, 0.5));
animation: popIn 0.55s 0.2s cubic-bezier(0.2, 1.4, 0.4, 1) both;
}
/* ===== Avatars ===== */
.avatars {
position: relative;
display: flex;
justify-content: center;
align-items: center;
gap: 0;
margin-bottom: 22px;
height: 130px;
}
.avatar { margin: 0; }
.avatar__img {
width: 116px;
height: 116px;
border-radius: 50%;
border: 4px solid var(--white);
background-size: cover;
background-position: center;
box-shadow: 0 16px 32px -14px rgba(0, 0, 0, 0.6);
}
.avatar--left { animation: slideFromLeft 0.7s 0.1s cubic-bezier(0.22, 1, 0.36, 1) both; }
.avatar--right { animation: slideFromRight 0.7s 0.1s cubic-bezier(0.22, 1, 0.36, 1) both; }
.avatar--left .avatar__img { margin-right: -18px; }
.avatar--right .avatar__img { margin-left: -18px; }
.avatar__img--you {
background-image:
url("https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=300&q=70");
}
.avatar__img--her {
background-image:
url("https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=300&q=70");
}
.avatar__name {
margin-top: 8px;
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
.heart-badge {
position: absolute;
left: 50%;
top: 46px;
transform: translate(-50%, -50%);
z-index: 3;
width: 58px;
height: 58px;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--grad);
box-shadow: 0 10px 26px -6px rgba(230, 57, 80, 0.7);
animation: heartPop 0.5s 0.6s cubic-bezier(0.2, 1.6, 0.4, 1) both;
}
/* ===== Copy + actions ===== */
.match__desc {
margin: 0 auto 24px;
max-width: 280px;
font-size: 15px;
color: rgba(255, 255, 255, 0.9);
animation: riseIn 0.5s 0.7s ease both;
}
.match__desc strong { color: var(--white); }
.match__actions {
display: grid;
gap: 12px;
animation: riseIn 0.5s 0.8s ease both;
}
.btn {
font: inherit;
font-weight: 700;
font-size: 16px;
padding: 15px 20px;
border-radius: 999px;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.2s ease, background 0.2s ease;
}
.btn:focus-visible {
outline: 3px solid #fff;
outline-offset: 3px;
}
.btn--primary {
border: none;
color: var(--white);
background: var(--grad);
box-shadow: 0 14px 30px -10px rgba(230, 57, 80, 0.8);
}
.btn--primary:hover { transform: translateY(-2px); box-shadow: 0 18px 36px -10px rgba(230, 57, 80, 0.9); }
.btn--primary:active { transform: translateY(0) scale(0.98); }
.btn--ghost {
border: 2px solid rgba(255, 255, 255, 0.75);
color: var(--white);
background: rgba(255, 255, 255, 0.08);
}
.btn--ghost:hover { background: rgba(255, 255, 255, 0.2); transform: translateY(-2px); }
.btn--ghost:active { transform: translateY(0) scale(0.98); }
/* ===== Confetti / hearts ===== */
.confetti {
position: absolute;
inset: 0;
z-index: 1;
overflow: hidden;
pointer-events: none;
}
.piece {
position: absolute;
top: -24px;
width: 10px;
height: 16px;
border-radius: 3px;
will-change: transform, opacity;
animation: fall linear forwards;
}
.piece--heart {
width: 18px;
height: 18px;
background: transparent !important;
color: var(--coral);
font-size: 18px;
line-height: 1;
border-radius: 0;
}
/* ===== Replay trigger ===== */
.replay {
font: inherit;
font-weight: 700;
font-size: 15px;
color: var(--white);
background: var(--grad);
border: none;
padding: 13px 24px;
border-radius: 999px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 16px 34px -12px rgba(139, 92, 246, 0.7);
transition: transform 0.15s ease, box-shadow 0.2s ease;
}
.replay:hover { transform: translateY(-2px); box-shadow: 0 22px 40px -12px rgba(139, 92, 246, 0.85); }
.replay:active { transform: translateY(0) scale(0.98); }
.replay:focus-visible { outline: 3px solid var(--violet-d); outline-offset: 3px; }
.replay__spark { font-size: 17px; }
/* ===== Toast ===== */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 20px);
z-index: 50;
background: var(--ink);
color: var(--white);
font-size: 14px;
font-weight: 600;
padding: 12px 20px;
border-radius: 999px;
box-shadow: 0 16px 34px -12px rgba(0, 0, 0, 0.5);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ===== Keyframes ===== */
@keyframes slideFromLeft {
from { transform: translateX(-120px) rotate(-10deg); opacity: 0; }
to { transform: translateX(0) rotate(0); opacity: 1; }
}
@keyframes slideFromRight {
from { transform: translateX(120px) rotate(10deg); opacity: 0; }
to { transform: translateX(0) rotate(0); opacity: 1; }
}
@keyframes heartPop {
0% { transform: translate(-50%, -50%) scale(0); }
70% { transform: translate(-50%, -50%) scale(1.25); }
100% { transform: translate(-50%, -50%) scale(1); }
}
@keyframes popIn {
0% { transform: scale(0.6); opacity: 0; }
70% { transform: scale(1.08); opacity: 1; }
100% { transform: scale(1); }
}
@keyframes riseIn {
from { transform: translateY(14px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes fall {
to { transform: translateY(760px) rotate(540deg); opacity: 0; }
}
/* ===== Responsive ===== */
@media (max-width: 520px) {
body { padding: 16px 10px; }
.phone {
width: 100%;
aspect-ratio: auto;
min-height: 92vh;
border-radius: 34px;
}
.match__title { font-size: 36px; }
.avatar__img { width: 100px; height: 100px; }
.swipe-card__photo { height: 250px; }
}
@media (prefers-reduced-motion: reduce) {
.overlay, .match__eyebrow, .match__title, .match__desc, .match__actions,
.avatar--left, .avatar--right, .heart-badge {
animation-duration: 0.001s !important;
animation-delay: 0s !important;
}
.piece { display: none; }
}(function () {
"use strict";
const overlay = document.getElementById("overlay");
const confetti = document.getElementById("confetti");
const replayBtn = document.getElementById("replayBtn");
const sendBtn = document.getElementById("sendBtn");
const keepBtn = document.getElementById("keepBtn");
const toastEl = document.getElementById("toast");
const match = overlay.querySelector(".match");
const MATCH_NAME = "Maya";
const reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
let toastTimer = null;
let lastFocus = null;
/* ---- Toast helper ---- */
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 2600);
}
/* ---- Confetti + heart burst ---- */
const COLORS = ["#ff5e6c", "#8b5cf6", "#ff8fb1", "#ffd166", "#7c3aed"];
function burst() {
if (reduceMotion) return;
confetti.innerHTML = "";
const total = 46;
for (let i = 0; i < total; i++) {
const isHeart = i % 4 === 0;
const p = document.createElement("span");
p.className = "piece" + (isHeart ? " piece--heart" : "");
p.style.left = Math.random() * 100 + "%";
const dur = 2.2 + Math.random() * 1.8;
p.style.animationDuration = dur + "s";
p.style.animationDelay = Math.random() * 0.4 + "s";
if (isHeart) {
p.textContent = "♥";
p.style.color = COLORS[i % COLORS.length];
p.style.fontSize = 12 + Math.random() * 14 + "px";
} else {
p.style.background = COLORS[i % COLORS.length];
p.style.height = 10 + Math.random() * 12 + "px";
p.style.opacity = "0.95";
}
confetti.appendChild(p);
}
// Clean up finished pieces
setTimeout(() => (confetti.innerHTML = ""), 4600);
}
/* ---- Open / replay the match animation ---- */
function openMatch() {
lastFocus = document.activeElement;
overlay.hidden = false;
// Re-trigger CSS entrance animations by reflow
overlay.style.animation = "none";
void overlay.offsetWidth;
overlay.style.animation = "";
reflowChildren();
burst();
// Focus primary action once avatars have met
setTimeout(() => sendBtn.focus(), reduceMotion ? 0 : 700);
}
function reflowChildren() {
const animated = match.querySelectorAll(
".match__eyebrow, .match__title, .match__desc, .match__actions, " +
".avatar--left, .avatar--right, .heart-badge"
);
animated.forEach((el) => {
el.style.animation = "none";
void el.offsetWidth;
el.style.animation = "";
});
}
function closeMatch() {
overlay.hidden = true;
confetti.innerHTML = "";
if (lastFocus && typeof lastFocus.focus === "function") {
lastFocus.focus();
} else {
replayBtn.focus();
}
}
/* ---- Focus trap while modal is open ---- */
function focusables() {
return Array.from(
overlay.querySelectorAll("button:not([disabled])")
).filter((el) => el.offsetParent !== null);
}
overlay.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
e.preventDefault();
closeMatch();
return;
}
if (e.key !== "Tab") return;
const items = focusables();
if (!items.length) return;
const first = items[0];
const last = items[items.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
// Backdrop click closes (but not clicks on the card itself)
overlay.addEventListener("click", (e) => {
if (e.target === overlay) closeMatch();
});
/* ---- Wire up controls ---- */
replayBtn.addEventListener("click", openMatch);
sendBtn.addEventListener("click", () => {
closeMatch();
toast("Chat opened with " + MATCH_NAME + " 💬");
});
keepBtn.addEventListener("click", () => {
closeMatch();
toast("Back to discovering ✨");
});
/* ---- Auto-play once on load for the demo ---- */
window.addEventListener("load", () => {
setTimeout(openMatch, 450);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Dating — Match Modal</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=Baloo+2:wght@600;700;800&display=swap"
rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- Phone frame — a live app screen behind the modal -->
<div class="phone" role="region" aria-label="Dating app screen">
<div class="phone__notch" aria-hidden="true"></div>
<header class="app-top">
<span class="app-top__logo">flare</span>
<span class="app-top__pill">Discover</span>
</header>
<!-- Card stack peeking behind the modal -->
<main class="deck" aria-hidden="true">
<article class="swipe-card">
<div class="swipe-card__photo swipe-card__photo--a"></div>
<div class="swipe-card__meta">
<h2>Maya, 27</h2>
<p>Ceramics · Trail runs · Oat lattes</p>
<div class="swipe-card__tags">
<span class="tag">Creative</span>
<span class="tag">Nearby</span>
<span class="tag tag--new">New</span>
</div>
</div>
</article>
</main>
<nav class="dock" aria-hidden="true">
<button class="dock__btn dock__btn--nope" tabindex="-1">✕</button>
<button class="dock__btn dock__btn--star" tabindex="-1">★</button>
<button class="dock__btn dock__btn--like" tabindex="-1">♥</button>
</nav>
<!-- ===== Match modal ===== -->
<div class="overlay" id="overlay" hidden>
<div class="confetti" id="confetti" aria-hidden="true"></div>
<div class="match" role="dialog" aria-modal="true"
aria-labelledby="matchTitle" aria-describedby="matchDesc">
<p class="match__eyebrow">You both liked each other</p>
<h1 class="match__title" id="matchTitle">It’s a Match!</h1>
<div class="avatars">
<figure class="avatar avatar--left">
<div class="avatar__img avatar__img--you"></div>
<figcaption class="avatar__name">You</figcaption>
</figure>
<span class="heart-badge" aria-hidden="true">
<svg viewBox="0 0 24 24" width="30" height="30" fill="#fff">
<path d="M12 21s-7.5-4.6-10-9.2C.3 8.4 1.7 5 5 5c2 0 3.2 1.1 4 2.3C9.8 6.1 11 5 13 5c3.3 0 4.7 3.4 3 6.8C19.5 16.4 12 21 12 21z"/>
</svg>
</span>
<figure class="avatar avatar--right">
<div class="avatar__img avatar__img--her"></div>
<figcaption class="avatar__name" id="matchName">Maya</figcaption>
</figure>
</div>
<p class="match__desc" id="matchDesc">
You and <strong id="matchNameInline">Maya</strong> are into each other.
Say hi before the spark fades.
</p>
<div class="match__actions">
<button class="btn btn--primary" id="sendBtn" type="button">
Send message
</button>
<button class="btn btn--ghost" id="keepBtn" type="button">
Keep swiping
</button>
</div>
</div>
</div>
</div>
<!-- Replay trigger -->
<button class="replay" id="replayBtn" type="button">
<span class="replay__spark" aria-hidden="true">✨</span>
Show match
</button>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>The Match Modal is the emotional peak of any dating app — the moment two people both swipe right. This component recreates that celebration as a self-contained overlay: a blurred, dimmed backdrop fades in, two round avatars slide from opposite edges to meet in the center, and a gradient heart badge pops between them with a spring bounce. At the same instant a burst of confetti and floating hearts rains down, generated entirely with vanilla JavaScript so there are no image or library dependencies.
Below the avatars a friendly headline reads “It’s a Match!” followed by the matched person’s name and a soft line of copy. Two pill buttons — a filled gradient Send message and an outlined Keep swiping — give the user a clear choice, both with hover, active and visible focus states for keyboard users. Selecting either closes the modal and raises a small toast confirming the action.
The card is wrapped in a ~430px phone frame so it feels like a real mobile
screen, and a floating trigger button lets you replay the whole animation on
demand. Everything scales down cleanly to 360px via a dedicated media query, the
overlay traps focus and closes on Escape or backdrop click, and all decorative
motion respects prefers-reduced-motion.