Dating — Notifications
A playful dating-app notifications feed inside a phone frame, grouping likes, matches and messages into swipeable rows with avatars, action text, relative timestamps and coral-to-violet unread highlights. Filter tabs let you narrow by type while live unread counts update, a mark-all-read button clears the glow, and per-row taps dismiss the badge with a springy micro-interaction and toast confirmation.
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;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(120% 80% at 15% 0%, rgba(255, 143, 177, 0.35), transparent 55%),
radial-gradient(120% 80% at 85% 100%, rgba(139, 92, 246, 0.25), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
padding: 32px 16px;
}
/* ---------- phone frame ---------- */
.phone {
width: 100%;
max-width: 430px;
background: var(--surface);
border-radius: var(--r-lg);
box-shadow:
0 2px 6px rgba(42, 26, 46, 0.06),
0 30px 70px -30px rgba(124, 58, 237, 0.45),
0 0 0 1px var(--line);
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ---------- header ---------- */
.app-head {
position: sticky;
top: 0;
z-index: 5;
background: var(--surface);
padding: 22px 20px 12px;
border-bottom: 1px solid var(--line);
}
.head-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.head-title h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 800;
letter-spacing: -0.02em;
background: var(--grad);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.unread-line {
margin: 2px 0 0;
font-size: 0.82rem;
font-weight: 500;
color: var(--muted);
}
.mark-all {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 6px;
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
color: var(--violet-d);
background: rgba(139, 92, 246, 0.10);
border: 1px solid rgba(139, 92, 246, 0.20);
padding: 8px 12px;
border-radius: 999px;
cursor: pointer;
transition: transform 0.14s ease, background 0.2s ease, opacity 0.2s ease;
}
.mark-all:hover { background: rgba(139, 92, 246, 0.18); }
.mark-all:active { transform: scale(0.94); }
.mark-all:disabled { opacity: 0.45; cursor: default; }
.mark-all svg { color: var(--violet-d); }
/* ---------- tabs ---------- */
.tabs {
display: flex;
gap: 6px;
margin-top: 16px;
overflow-x: auto;
scrollbar-width: none;
padding-bottom: 2px;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 6px;
font-family: inherit;
font-size: 0.82rem;
font-weight: 600;
color: var(--ink-2);
background: transparent;
border: 1px solid var(--line);
padding: 7px 13px;
border-radius: 999px;
cursor: pointer;
transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease, transform 0.14s ease;
}
.tab:hover { border-color: rgba(255, 94, 108, 0.4); color: var(--ink); }
.tab:active { transform: scale(0.95); }
.tab.is-active {
color: var(--white);
background: var(--grad);
border-color: transparent;
box-shadow: 0 6px 16px -6px rgba(230, 57, 80, 0.6);
}
.tab-count {
min-width: 18px;
height: 18px;
padding: 0 5px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.68rem;
font-weight: 700;
border-radius: 999px;
background: rgba(42, 26, 46, 0.08);
color: var(--ink-2);
}
.tab.is-active .tab-count {
background: rgba(255, 255, 255, 0.28);
color: var(--white);
}
.tab-count[data-empty="true"] { display: none; }
/* ---------- feed ---------- */
.feed {
display: flex;
flex-direction: column;
padding: 8px 12px 20px;
gap: 4px;
max-height: 560px;
overflow-y: auto;
}
.group-label {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
padding: 14px 10px 6px;
}
/* ---------- row ---------- */
.row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 12px;
border-radius: var(--r-md);
background: var(--surface);
border: 1px solid transparent;
cursor: pointer;
text-align: left;
width: 100%;
font-family: inherit;
transition: transform 0.14s ease, background 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
animation: rowIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes rowIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.row:hover { background: rgba(255, 94, 108, 0.05); }
.row:active { transform: scale(0.985); }
.row:focus-visible {
outline: none;
border-color: var(--violet);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.28);
}
.row.is-unread {
background: linear-gradient(135deg, rgba(255, 94, 108, 0.10), rgba(139, 92, 246, 0.10));
border-color: rgba(255, 94, 108, 0.16);
}
.row.is-unread:hover {
background: linear-gradient(135deg, rgba(255, 94, 108, 0.16), rgba(139, 92, 246, 0.16));
}
/* avatar */
.avatar {
position: relative;
flex-shrink: 0;
width: 52px;
height: 52px;
}
.avatar-img {
width: 52px;
height: 52px;
border-radius: 50%;
object-fit: cover;
display: block;
background: var(--grad);
}
.row.is-unread .avatar-img {
box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--coral);
}
.type-badge {
position: absolute;
right: -2px;
bottom: -2px;
width: 22px;
height: 22px;
border-radius: 50%;
display: grid;
place-items: center;
border: 2px solid var(--surface);
color: var(--white);
}
.type-badge svg { width: 12px; height: 12px; }
.type-badge.like { background: var(--coral); }
.type-badge.match { background: linear-gradient(135deg, var(--coral), var(--violet)); }
.type-badge.message { background: var(--violet); }
/* body */
.row-body { flex: 1; min-width: 0; }
.row-text {
margin: 0;
font-size: 0.9rem;
color: var(--ink-2);
font-weight: 500;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.row-text strong {
color: var(--ink);
font-weight: 700;
}
.row.is-unread .row-text { color: var(--ink); }
.row-time {
margin: 3px 0 0;
font-size: 0.74rem;
font-weight: 500;
color: var(--muted);
}
/* trailing */
.row-end {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 8px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--coral);
box-shadow: 0 0 0 0 rgba(255, 94, 108, 0.5);
animation: pulse 2s infinite;
}
.row:not(.is-unread) .dot { display: none; }
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 94, 108, 0.5); }
70% { box-shadow: 0 0 0 7px rgba(255, 94, 108, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 94, 108, 0); }
}
.chip {
font-size: 0.68rem;
font-weight: 700;
padding: 4px 9px;
border-radius: 999px;
background: var(--grad);
color: var(--white);
white-space: nowrap;
}
.feed-empty {
text-align: center;
color: var(--muted);
font-weight: 600;
padding: 40px 20px 48px;
margin: 0;
}
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 20px);
background: var(--ink);
color: var(--white);
font-size: 0.82rem;
font-weight: 600;
padding: 11px 18px;
border-radius: 999px;
box-shadow: 0 14px 34px -10px rgba(42, 26, 46, 0.55);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- responsive ---------- */
@media (max-width: 520px) {
body { padding: 0; align-items: stretch; }
.phone {
max-width: 100%;
min-height: 100vh;
border-radius: 0;
box-shadow: none;
}
.feed { max-height: none; flex: 1; }
.head-title h1 { font-size: 1.35rem; }
.mark-all span { display: inline; }
.avatar, .avatar-img { width: 48px; height: 48px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; }
}(function () {
"use strict";
var AV = "https://images.unsplash.com/";
var P = "?auto=format&fit=crop&w=140&h=140&q=70";
// Fictional but realistic notification data.
var data = [
{ id: 1, type: "match", name: "Mara", unread: true, mins: 3,
photo: AV + "photo-1544005313-94ddf0286df2" + P,
text: "It's a match! You and <strong>Mara</strong> liked each other 💘", chip: "New match" },
{ id: 2, type: "message", name: "Diego", unread: true, mins: 11,
photo: AV + "photo-1500648767791-00dcc994a43e" + P,
text: "<strong>Diego</strong> sent you a message: “Coffee this weekend?”" },
{ id: 3, type: "like", name: "Priya", unread: true, mins: 26,
photo: AV + "photo-1534528741775-53994a69daeb" + P,
text: "<strong>Priya</strong> liked your profile" },
{ id: 4, type: "match", name: "Noah", unread: true, mins: 48,
photo: AV + "photo-1502823403499-6ccfcf4fb453" + P,
text: "You matched with <strong>Noah</strong> — say hi first!", chip: "New match" },
{ id: 5, type: "like", name: "Aisha", unread: true, mins: 92,
photo: AV + "photo-1531123897727-8f129e1688ce" + P,
text: "<strong>Aisha</strong> and 3 others liked you" },
{ id: 6, type: "message", name: "Leo", unread: false, mins: 180,
photo: AV + "photo-1506794778202-cad84cf45f1d" + P,
text: "<strong>Leo</strong>: “Loved your hiking photos 🏔️”" },
{ id: 7, type: "like", name: "Sofia", unread: false, mins: 320,
photo: AV + "photo-1524504388940-b1c1722653e1" + P,
text: "<strong>Sofia</strong> super-liked your profile ⭐" },
{ id: 8, type: "match", name: "Kenji", unread: false, mins: 1440,
photo: AV + "photo-1507003211169-0a1dd7228f2d" + P,
text: "You matched with <strong>Kenji</strong> yesterday" },
{ id: 9, type: "message", name: "Yara", unread: false, mins: 2880,
photo: AV + "photo-1508214751196-bcfd4ca60f91" + P,
text: "<strong>Yara</strong>: “Haha same! See you Friday”" }
];
var ICONS = {
like: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 21s-7.5-4.6-9.7-9.1C.9 8.9 2.3 5.6 5.4 5.1c1.9-.3 3.6.7 4.6 2.1 1-1.4 2.7-2.4 4.6-2.1 3.1.5 4.5 3.8 3.1 6.8C19.5 16.4 12 21 12 21z"/></svg>',
match: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 21s-7.5-4.6-9.7-9.1C.9 8.9 2.3 5.6 5.4 5.1c1.9-.3 3.6.7 4.6 2.1 1-1.4 2.7-2.4 4.6-2.1 3.1.5 4.5 3.8 3.1 6.8C19.5 16.4 12 21 12 21z"/></svg>',
message: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4 4h16v12H7l-3 3V4z"/></svg>'
};
var TYPE_LABEL = { like: "Likes", match: "Matches", message: "Messages" };
var feed = document.getElementById("feed");
var feedEmpty = document.getElementById("feedEmpty");
var unreadLine = document.getElementById("unreadLine");
var markAll = document.getElementById("markAll");
var toastEl = document.getElementById("toast");
var tabs = Array.prototype.slice.call(document.querySelectorAll(".tab"));
var currentFilter = "all";
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
function relTime(mins) {
if (mins < 1) return "just now";
if (mins < 60) return mins + "m ago";
var h = Math.round(mins / 60);
if (h < 24) return h + "h ago";
var d = Math.round(h / 24);
return d === 1 ? "yesterday" : d + "d ago";
}
function groupOf(mins) {
if (mins <= 60) return "New";
if (mins <= 1440) return "Today";
return "Earlier";
}
function countUnread(type) {
return data.filter(function (n) {
return n.unread && (!type || type === "all" || n.type === type);
}).length;
}
function updateCounts() {
document.querySelectorAll(".tab-count").forEach(function (el) {
var type = el.getAttribute("data-count");
var c = countUnread(type);
el.textContent = c;
el.setAttribute("data-empty", c === 0 ? "true" : "false");
});
var total = countUnread("all");
unreadLine.textContent = total === 0
? "You're all caught up ✨"
: "You have " + total + " new update" + (total === 1 ? "" : "s");
markAll.disabled = total === 0;
}
function render() {
var rows = data.filter(function (n) {
return currentFilter === "all" || n.type === currentFilter;
});
feed.innerHTML = "";
if (rows.length === 0) {
feedEmpty.hidden = false;
updateCounts();
return;
}
feedEmpty.hidden = true;
var lastGroup = null;
rows.forEach(function (n, i) {
var g = groupOf(n.mins);
if (g !== lastGroup) {
lastGroup = g;
var lbl = document.createElement("div");
lbl.className = "group-label";
lbl.textContent = g;
feed.appendChild(lbl);
}
var btn = document.createElement("button");
btn.type = "button";
btn.className = "row" + (n.unread ? " is-unread" : "");
btn.style.animationDelay = (i * 0.035) + "s";
btn.setAttribute("data-id", String(n.id));
btn.setAttribute("aria-label",
n.name + ", " + TYPE_LABEL[n.type] + ", " + relTime(n.mins) +
(n.unread ? ", unread" : ""));
btn.innerHTML =
'<span class="avatar">' +
'<img class="avatar-img" src="' + n.photo + '" alt="" loading="lazy" ' +
'onerror="this.style.visibility="hidden"">' +
'<span class="type-badge ' + n.type + '">' + ICONS[n.type] + '</span>' +
'</span>' +
'<span class="row-body">' +
'<p class="row-text">' + n.text + '</p>' +
'<p class="row-time">' + relTime(n.mins) + '</p>' +
'</span>' +
'<span class="row-end">' +
(n.chip ? '<span class="chip">' + n.chip + '</span>' : '') +
'<span class="dot" aria-hidden="true"></span>' +
'</span>';
feed.appendChild(btn);
});
updateCounts();
}
// Read a single row on tap.
feed.addEventListener("click", function (e) {
var row = e.target.closest(".row");
if (!row) return;
var id = parseInt(row.getAttribute("data-id"), 10);
var item = data.find(function (n) { return n.id === id; });
if (!item) return;
if (item.unread) {
item.unread = false;
row.classList.remove("is-unread");
updateCounts();
toast("Marked as read");
} else {
toast("Opening chat with " + item.name + "…");
}
});
// Mark all read.
markAll.addEventListener("click", function () {
var any = data.some(function (n) { return n.unread; });
if (!any) return;
data.forEach(function (n) { n.unread = false; });
document.querySelectorAll(".row.is-unread").forEach(function (r) {
r.classList.remove("is-unread");
});
updateCounts();
toast("All caught up! 💌");
});
// Filter tabs.
tabs.forEach(function (tab) {
tab.addEventListener("click", function () {
tabs.forEach(function (t) {
t.classList.remove("is-active");
t.setAttribute("aria-pressed", "false");
});
tab.classList.add("is-active");
tab.setAttribute("aria-pressed", "true");
currentFilter = tab.getAttribute("data-filter");
render();
});
});
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Dating — Notifications</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="phone" aria-label="Dating app notifications">
<header class="app-head">
<div class="head-top">
<div class="head-title">
<h1>Activity</h1>
<p id="unreadLine" class="unread-line">You have 5 new updates</p>
</div>
<button id="markAll" class="mark-all" type="button">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M4 12.5 9 17.5 20 6.5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Mark all read
</button>
</div>
<nav class="tabs" aria-label="Filter notifications">
<button class="tab is-active" type="button" data-filter="all" aria-pressed="true">
All <span class="tab-count" data-count="all">12</span>
</button>
<button class="tab" type="button" data-filter="like" aria-pressed="false">
Likes <span class="tab-count" data-count="like">0</span>
</button>
<button class="tab" type="button" data-filter="match" aria-pressed="false">
Matches <span class="tab-count" data-count="match">0</span>
</button>
<button class="tab" type="button" data-filter="message" aria-pressed="false">
Messages <span class="tab-count" data-count="message">0</span>
</button>
</nav>
</header>
<section class="feed" id="feed" aria-label="Notifications list" aria-live="polite">
<!-- rows injected by script.js -->
</section>
<p class="feed-empty" id="feedEmpty" hidden>No notifications here yet 💫</p>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A mobile-first notifications feed for a dating app, wrapped in a ~430px phone frame centered on a soft blush background. Each row pairs a gradient-ringed avatar with an action line (someone liked you, a new match, a fresh message), a relative timestamp, and a type icon. Unread rows glow with a coral-to-violet wash and carry a dot badge, so the eye lands on what is new first.
A sticky header shows the total unread count and a Mark all read control. Below it, a row of filter tabs — All, Likes, Matches, Messages — narrows the feed with animated pill selection and per-tab counters that recompute as you read. Tapping any unread row clears its badge, decrements the counters, and fires a small toast; a springy scale on press makes each interaction feel tactile.
Everything is vanilla HTML, CSS and JavaScript: no frameworks, no build step. The layout stays comfortable down to 360px, controls are keyboard reachable with visible focus rings, and colors meet WCAG AA contrast against the light surface.