Dating — Chat Messages
A playful dating-app chat screen wrapped in a phone frame: a scrollable conversation list with avatars, unread dots and last-message previews, an active thread of gradient chat bubbles with delivery ticks and timestamps, an animated three-dot typing indicator, a fresh-match banner up top, and a rounded send bar with an emoji picker that scripts realistic auto-replies.
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 {
margin: 0;
padding: 0;
}
body {
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(120% 80% at 15% 0%, #ffe1e7 0%, transparent 55%),
radial-gradient(120% 90% at 90% 100%, #ece0ff 0%, transparent 50%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px 12px;
}
/* Phone frame */
.phone {
position: relative;
width: 430px;
max-width: 100%;
height: 800px;
max-height: calc(100vh - 48px);
background: var(--surface);
border-radius: 40px;
box-shadow:
0 2px 0 rgba(255, 255, 255, 0.6) inset,
0 30px 70px -20px rgba(124, 58, 237, 0.35),
0 10px 30px -10px rgba(230, 57, 80, 0.25);
border: 8px solid #17111c;
overflow: hidden;
display: grid;
grid-template-columns: 168px 1fr;
}
.phone__notch {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 132px;
height: 24px;
background: #17111c;
border-radius: 0 0 16px 16px;
z-index: 40;
}
/* ---------- Conversation rail ---------- */
.rail {
background: linear-gradient(180deg, #fff 0%, #fff8f9 100%);
border-right: 1px solid var(--line);
display: flex;
flex-direction: column;
min-height: 0;
}
.rail__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 34px 12px 8px;
}
.rail__title {
display: flex;
align-items: center;
gap: 6px;
}
.rail__title h1 {
font-family: "Poppins", sans-serif;
font-size: 17px;
font-weight: 800;
margin: 0;
letter-spacing: -0.3px;
}
.logo {
display: grid;
place-items: center;
width: 22px;
height: 22px;
border-radius: 8px;
background: var(--grad);
color: #fff;
font-size: 13px;
}
.icon-btn {
border: 0;
background: rgba(139, 92, 246, 0.10);
color: var(--violet-d);
width: 34px;
height: 34px;
border-radius: 50%;
font-size: 16px;
cursor: pointer;
display: grid;
place-items: center;
transition: transform 0.15s, background 0.2s, box-shadow 0.2s;
}
.icon-btn:hover { background: rgba(139, 92, 246, 0.18); transform: translateY(-1px); }
.icon-btn:active { transform: scale(0.92); }
.icon-btn:focus-visible { outline: 3px solid var(--pink); outline-offset: 2px; }
.search {
position: relative;
margin: 6px 12px 8px;
}
.search__icon {
position: absolute;
left: 11px;
top: 50%;
transform: translateY(-50%);
color: var(--muted);
font-size: 15px;
}
.search input {
width: 100%;
border: 1px solid var(--line);
background: var(--white);
border-radius: 999px;
padding: 8px 12px 8px 30px;
font: inherit;
font-size: 13px;
color: var(--ink);
}
.search input:focus-visible {
outline: none;
border-color: var(--violet);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.18);
}
.newmatches { padding: 4px 12px 10px; border-bottom: 1px solid var(--line); }
.newmatches__label {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.6px;
color: var(--muted);
margin: 0 0 8px;
}
.newmatches__row {
display: flex;
gap: 10px;
overflow-x: auto;
padding-bottom: 4px;
scrollbar-width: none;
}
.newmatches__row::-webkit-scrollbar { display: none; }
.newmatch {
flex: 0 0 auto;
border: 0;
background: none;
cursor: pointer;
text-align: center;
padding: 0;
}
.newmatch .avatar {
border: 2px solid transparent;
background-clip: padding-box;
box-shadow: 0 0 0 2px var(--coral);
transition: transform 0.15s, box-shadow 0.2s;
}
.newmatch:hover .avatar { transform: scale(1.06); box-shadow: 0 0 0 2px var(--violet); }
.newmatch small {
display: block;
font-size: 10px;
color: var(--ink-2);
margin-top: 3px;
max-width: 44px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.convos {
list-style: none;
margin: 0;
padding: 4px 0 12px;
overflow-y: auto;
flex: 1;
min-height: 0;
scrollbar-width: thin;
}
.convo {
display: grid;
grid-template-columns: 40px 1fr auto;
gap: 8px;
align-items: center;
padding: 9px 10px;
cursor: pointer;
border: 0;
background: none;
width: 100%;
text-align: left;
font: inherit;
border-left: 3px solid transparent;
transition: background 0.15s;
}
.convo:hover { background: rgba(255, 94, 108, 0.06); }
.convo.is-active {
background: rgba(139, 92, 246, 0.10);
border-left-color: var(--violet);
}
.convo:focus-visible { outline: 2px solid var(--violet); outline-offset: -2px; }
.convo__wrap { position: relative; }
.convo__body { min-width: 0; }
.convo__top {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 6px;
}
.convo__name {
font-weight: 700;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.convo__time { font-size: 10px; color: var(--muted); flex: 0 0 auto; }
.convo__preview {
font-size: 12px;
color: var(--ink-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 1px 0 0;
}
.convo.is-unread .convo__preview { color: var(--ink); font-weight: 600; }
.convo__meta { display: grid; place-items: center; min-width: 20px; }
.unread-dot {
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 999px;
background: var(--grad);
color: #fff;
font-size: 10px;
font-weight: 700;
display: grid;
place-items: center;
box-shadow: 0 3px 8px rgba(230, 57, 80, 0.4);
}
/* Avatars */
.avatar {
display: inline-block;
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--grad);
position: relative;
flex: 0 0 auto;
color: #fff;
font-weight: 700;
font-size: 15px;
text-align: center;
line-height: 40px;
}
.avatar--sm { width: 36px; height: 36px; line-height: 36px; font-size: 14px; }
.avatar--xs { width: 26px; height: 26px; line-height: 26px; font-size: 11px; }
.avatar[data-online="1"]::after {
content: "";
position: absolute;
right: -1px;
bottom: -1px;
width: 11px;
height: 11px;
border-radius: 50%;
background: #22c55e;
border: 2px solid var(--surface);
}
/* ---------- Chat panel ---------- */
.chat {
display: flex;
flex-direction: column;
min-height: 0;
background:
radial-gradient(120% 60% at 100% 0%, rgba(139, 92, 246, 0.06), transparent 60%),
#fffdfd;
}
.chat__head {
display: flex;
align-items: center;
gap: 8px;
padding: 32px 12px 10px;
border-bottom: 1px solid var(--line);
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(6px);
}
.back { display: none; }
.chat__peer { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
.chat__peerinfo { min-width: 0; }
.chat__name { margin: 0; font-weight: 700; font-size: 14px; }
.chat__status {
margin: 0;
font-size: 11px;
color: var(--muted);
display: flex;
align-items: center;
gap: 5px;
}
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }
.dot--on { background: #22c55e; box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18); }
.chat__actions { display: flex; gap: 6px; }
.chat__actions .icon-btn { background: rgba(255, 94, 108, 0.10); color: var(--coral-d); }
.chat__actions .icon-btn:hover { background: rgba(255, 94, 108, 0.18); }
.matchbanner {
margin: 10px 12px 2px;
padding: 10px 12px;
border-radius: var(--r-md);
background: linear-gradient(135deg, rgba(255, 94, 108, 0.14), rgba(139, 92, 246, 0.14));
border: 1px solid rgba(139, 92, 246, 0.20);
display: flex;
align-items: center;
gap: 9px;
font-size: 12px;
color: var(--ink-2);
}
.matchbanner strong { color: var(--coral-d); }
.matchbanner__spark {
font-size: 18px;
animation: sparkle 2.2s ease-in-out infinite;
}
@keyframes sparkle {
0%, 100% { transform: scale(1) rotate(0); opacity: 1; }
50% { transform: scale(1.2) rotate(12deg); opacity: 0.7; }
}
.thread {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 12px 12px 4px;
display: flex;
flex-direction: column;
gap: 4px;
scrollbar-width: thin;
}
.daysep {
align-self: center;
font-size: 10px;
color: var(--muted);
background: rgba(42, 26, 46, 0.05);
padding: 3px 10px;
border-radius: 999px;
margin: 8px 0;
font-weight: 600;
}
.msg {
max-width: 74%;
padding: 9px 13px;
font-size: 13px;
border-radius: 18px;
position: relative;
word-wrap: break-word;
animation: pop 0.22s ease;
}
@keyframes pop {
from { opacity: 0; transform: translateY(8px) scale(0.96); }
to { opacity: 1; transform: none; }
}
.msg--in {
align-self: flex-start;
background: #fff;
border: 1px solid var(--line);
color: var(--ink);
border-bottom-left-radius: 6px;
}
.msg--out {
align-self: flex-end;
background: var(--grad);
color: #fff;
border-bottom-right-radius: 6px;
box-shadow: 0 6px 16px -6px rgba(230, 57, 80, 0.5);
}
.msg__meta {
display: flex;
align-items: center;
gap: 4px;
justify-content: flex-end;
font-size: 9px;
margin-top: 3px;
opacity: 0.8;
}
.msg--in .msg__meta { color: var(--muted); }
.msg--out .msg__meta { color: rgba(255, 255, 255, 0.9); }
.tick { font-size: 10px; letter-spacing: -2px; }
.tick.read { color: #b9f6ca; }
/* Typing indicator */
.typing {
display: flex;
align-items: center;
gap: 8px;
padding: 2px 12px 6px;
}
.typing__bubble {
background: #fff;
border: 1px solid var(--line);
border-radius: 16px;
border-bottom-left-radius: 6px;
padding: 10px 12px;
display: flex;
gap: 4px;
}
.typing__bubble span {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--violet);
animation: blink 1.3s infinite ease-in-out;
}
.typing__bubble span:nth-child(2) { animation-delay: 0.2s; }
.typing__bubble span:nth-child(3) { animation-delay: 0.4s; }
@keyframes blink {
0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }
40% { opacity: 1; transform: translateY(-3px); }
}
/* Composer */
.composer {
border-top: 1px solid var(--line);
padding: 8px 10px 12px;
background: rgba(255, 255, 255, 0.9);
}
.emoji-tray {
display: flex;
flex-wrap: wrap;
gap: 2px;
padding: 8px;
margin-bottom: 8px;
background: #fff;
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: 0 10px 24px -12px rgba(124, 58, 237, 0.3);
}
.emoji-tray button {
border: 0;
background: none;
font-size: 20px;
cursor: pointer;
border-radius: 10px;
padding: 4px 6px;
transition: transform 0.12s, background 0.15s;
}
.emoji-tray button:hover { background: rgba(255, 94, 108, 0.12); transform: scale(1.2); }
.emoji-tray button:focus-visible { outline: 2px solid var(--violet); }
.composer__bar {
display: flex;
align-items: flex-end;
gap: 8px;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 5px 6px 5px 8px;
}
.composer__bar:focus-within {
border-color: var(--violet);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
}
.emoji-btn { background: transparent; color: var(--coral); flex: 0 0 auto; }
.emoji-btn:hover { background: rgba(255, 94, 108, 0.12); }
#msgInput {
flex: 1;
border: 0;
resize: none;
font: inherit;
font-size: 13px;
background: transparent;
color: var(--ink);
padding: 7px 2px;
max-height: 90px;
line-height: 1.4;
}
#msgInput:focus { outline: none; }
.send-btn {
flex: 0 0 auto;
border: 0;
width: 38px;
height: 38px;
border-radius: 50%;
background: var(--grad);
color: #fff;
font-size: 15px;
cursor: pointer;
display: grid;
place-items: center;
transition: transform 0.15s, box-shadow 0.2s, opacity 0.2s;
box-shadow: 0 6px 14px -4px rgba(230, 57, 80, 0.55);
}
.send-btn:hover { transform: translateY(-1px) scale(1.05); }
.send-btn:active { transform: scale(0.9); }
.send-btn:focus-visible { outline: 3px solid var(--pink); outline-offset: 2px; }
.send-btn:disabled { opacity: 0.45; cursor: default; box-shadow: none; }
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translateX(-50%) translateY(20px);
background: var(--ink);
color: #fff;
padding: 10px 18px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 100;
box-shadow: 0 12px 30px -8px rgba(0, 0, 0, 0.4);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---------- Responsive: single column app nav ---------- */
@media (max-width: 520px) {
body { padding: 0; }
.phone {
width: 100%;
height: 100vh;
max-height: none;
border-radius: 0;
border-width: 0;
grid-template-columns: 1fr;
}
.phone__notch { display: none; }
.rail, .chat { grid-column: 1; grid-row: 1; }
.rail__head { padding-top: 18px; }
.chat__head { padding-top: 18px; }
.chat {
transform: translateX(100%);
transition: transform 0.28s ease;
box-shadow: -12px 0 30px -12px rgba(0, 0, 0, 0.2);
}
.phone.show-chat .chat { transform: translateX(0); }
.phone.show-chat .rail { pointer-events: none; }
.back { display: grid; }
.msg { max-width: 80%; }
}(function () {
"use strict";
// ---------- Data ----------
var conversations = [
{
id: "mia",
name: "Mia Reyes",
initials: "MR",
grad: "linear-gradient(135deg,#ff5e6c,#8b5cf6)",
online: true,
status: "Active now",
unread: 2,
matched: "Jun 30",
time: "9:41",
messages: [
{ from: "them", text: "Okay your dog photo genuinely made my day 🐶", t: "9:12" },
{ from: "them", text: "What's his name??", t: "9:12" },
{ from: "me", text: "Ha! That's Biscuit — professional nap enthusiast 😄", t: "9:20", read: true },
{ from: "them", text: "Biscuit is such a perfect name", t: "9:38" },
{ from: "them", text: "So… coffee this weekend, or are you more of a wine person?", t: "9:41" }
]
},
{
id: "leo",
name: "Leo Marchetti",
initials: "LM",
grad: "linear-gradient(135deg,#8b5cf6,#ff8fb1)",
online: false,
status: "Active 12m ago",
unread: 0,
matched: "Jun 28",
time: "Yesterday",
messages: [
{ from: "me", text: "That climbing gym you mentioned — is it beginner friendly?", t: "18:02", read: true },
{ from: "them", text: "Totally! I can show you the ropes 😏", t: "18:20" },
{ from: "me", text: "Was that a pun", t: "18:21", read: true },
{ from: "them", text: "…maybe. Saturday?", t: "18:22" }
]
},
{
id: "priya",
name: "Priya Anand",
initials: "PA",
grad: "linear-gradient(135deg,#ff8fb1,#ff5e6c)",
online: true,
status: "Active now",
unread: 1,
matched: "Jun 27",
time: "8:03",
messages: [
{ from: "them", text: "Your bookshelf photo… is that the whole Le Guin collection?", t: "7:50" },
{ from: "me", text: "Guilty. I reread The Dispossessed every winter 📚", t: "7:58", read: true },
{ from: "them", text: "Marry me. (Kidding. Sort of.)", t: "8:03" }
]
},
{
id: "sam",
name: "Sam Okafor",
initials: "SO",
grad: "linear-gradient(135deg,#7c3aed,#ff5e6c)",
online: false,
status: "Active 2h ago",
unread: 0,
matched: "Jun 25",
time: "Mon",
messages: [
{ from: "them", text: "Sending you the playlist I promised 🎧", t: "Mon" },
{ from: "me", text: "Track 4 already living in my head rent free", t: "Mon", read: false }
]
},
{
id: "noah",
name: "Noah Bennett",
initials: "NB",
grad: "linear-gradient(135deg,#ff5e6c,#ff8fb1)",
online: false,
status: "Active 1d ago",
unread: 0,
matched: "Jun 22",
time: "Sun",
messages: [
{ from: "me", text: "Farmers market Sunday was so much fun 🍓", t: "Sun", read: true },
{ from: "them", text: "The peach guy waved at us like we're regulars now", t: "Sun" }
]
}
];
var replyPool = [
"Okay that's actually adorable 🥹",
"Haha you're trouble, I can tell 😄",
"Wait, say more — I'm intrigued",
"You had me at that emoji ✨",
"Coffee it is then. I know the perfect spot ☕",
"Stop, you're too smooth for a Tuesday 💜",
"I'm smiling at my phone like a goof rn",
"Okay pitch me your ideal first date 👀"
];
var emojis = ["😀","😄","😍","🥰","😘","😉","😎","🤩","😂","🥹","🙃","😇",
"💜","❤️","💕","💘","✨","🔥","🌸","🌹","☕","🍷","🍓","🎧","📚","🐶","😏","👀","🙌","💫"];
// ---------- Elements ----------
var phone = document.querySelector(".phone");
var convosEl = document.getElementById("convos");
var newMatchesEl = document.getElementById("newMatches");
var threadEl = document.getElementById("thread");
var typingEl = document.getElementById("typing");
var typingAvatar = document.getElementById("typingAvatar");
var peerName = document.getElementById("peerName");
var peerStatus = document.getElementById("peerStatus");
var peerAvatar = document.getElementById("peerAvatar");
var bannerName = document.getElementById("bannerName");
var input = document.getElementById("msgInput");
var sendBtn = document.getElementById("sendBtn");
var emojiBtn = document.getElementById("emojiBtn");
var emojiTray = document.getElementById("emojiTray");
var searchEl = document.getElementById("convoSearch");
var backBtn = document.getElementById("backBtn");
var toastEl = document.getElementById("toast");
var activeId = conversations[0].id;
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 1900);
}
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
function getConvo(id) {
for (var i = 0; i < conversations.length; i++) {
if (conversations[i].id === id) return conversations[i];
}
return conversations[0];
}
// ---------- Render new matches strip ----------
function renderNewMatches() {
newMatchesEl.innerHTML = "";
conversations.forEach(function (c) {
var btn = document.createElement("button");
btn.className = "newmatch";
btn.type = "button";
btn.setAttribute("aria-label", "Open chat with " + c.name);
btn.innerHTML =
'<span class="avatar" style="background:' + c.grad + '"' +
(c.online ? ' data-online="1"' : "") + ">" + esc(c.initials) + "</span>" +
"<small>" + esc(c.name.split(" ")[0]) + "</small>";
btn.addEventListener("click", function () { openConversation(c.id); });
newMatchesEl.appendChild(btn);
});
}
// ---------- Render conversation list ----------
function renderConvos(filter) {
filter = (filter || "").toLowerCase();
convosEl.innerHTML = "";
conversations.forEach(function (c) {
if (filter && c.name.toLowerCase().indexOf(filter) === -1) return;
var last = c.messages[c.messages.length - 1];
var preview = (last.from === "me" ? "You: " : "") + last.text;
var li = document.createElement("li");
var btn = document.createElement("button");
btn.className = "convo" +
(c.id === activeId ? " is-active" : "") +
(c.unread > 0 ? " is-unread" : "");
btn.type = "button";
btn.setAttribute("role", "option");
btn.setAttribute("aria-selected", c.id === activeId ? "true" : "false");
btn.innerHTML =
'<span class="convo__wrap"><span class="avatar" style="background:' + c.grad + '"' +
(c.online ? ' data-online="1"' : "") + ">" + esc(c.initials) + "</span></span>" +
'<span class="convo__body">' +
'<span class="convo__top">' +
'<span class="convo__name">' + esc(c.name) + "</span>" +
'<span class="convo__time">' + esc(c.time) + "</span>" +
"</span>" +
'<span class="convo__preview">' + esc(preview) + "</span>" +
"</span>" +
'<span class="convo__meta">' +
(c.unread > 0 ? '<span class="unread-dot">' + c.unread + "</span>" : "") +
"</span>";
btn.addEventListener("click", function () { openConversation(c.id); });
li.appendChild(btn);
convosEl.appendChild(li);
});
}
// ---------- Render active thread ----------
function renderThread() {
var c = getConvo(activeId);
peerName.textContent = c.name;
peerAvatar.style.background = c.grad;
peerAvatar.textContent = c.initials;
if (c.online) { peerAvatar.setAttribute("data-online", "1"); }
else { peerAvatar.removeAttribute("data-online"); }
peerStatus.innerHTML =
'<span class="dot ' + (c.online ? "dot--on" : "") + '"></span> ' + esc(c.status);
bannerName.textContent = c.name.split(" ")[0];
typingAvatar.style.background = c.grad;
typingAvatar.textContent = c.initials;
threadEl.innerHTML = '<span class="daysep">Matched ' + esc(c.matched) + "</span>";
c.messages.forEach(function (m) { threadEl.appendChild(buildMsg(m)); });
scrollBottom();
}
function buildMsg(m) {
var div = document.createElement("div");
div.className = "msg " + (m.from === "me" ? "msg--out" : "msg--in");
var meta = '<span class="msg__meta"><span>' + esc(m.t) + "</span>";
if (m.from === "me") {
meta += '<span class="tick ' + (m.read ? "read" : "") + '">' +
(m.read ? "✓✓" : "✓") + "</span>";
}
meta += "</span>";
div.innerHTML = "<span>" + esc(m.text) + "</span>" + meta;
return div;
}
function scrollBottom() {
requestAnimationFrame(function () { threadEl.scrollTop = threadEl.scrollHeight; });
}
function nowTime() {
var d = new Date();
var h = d.getHours();
var m = d.getMinutes();
return (h < 10 ? "0" + h : h) + ":" + (m < 10 ? "0" + m : m);
}
// ---------- Open a conversation ----------
function openConversation(id) {
activeId = id;
var c = getConvo(id);
c.unread = 0;
renderConvos(searchEl.value);
renderThread();
phone.classList.add("show-chat");
input.focus();
}
// ---------- Send flow ----------
function sendMessage() {
var text = input.value.trim();
if (!text) return;
var c = getConvo(activeId);
var msg = { from: "me", text: text, t: nowTime(), read: false };
c.messages.push(msg);
threadEl.appendChild(buildMsg(msg));
input.value = "";
autoGrow();
updateSendState();
scrollBottom();
renderConvos(searchEl.value);
// mark read + reply
setTimeout(function () {
msg.read = true;
renderThread();
}, 900);
setTimeout(function () {
typingEl.hidden = false;
scrollBottom();
}, 1300);
setTimeout(function () {
typingEl.hidden = true;
var reply = {
from: "them",
text: replyPool[Math.floor(Math.random() * replyPool.length)],
t: nowTime()
};
c.messages.push(reply);
// only append if still viewing this convo
if (c.id === activeId) {
threadEl.appendChild(buildMsg(reply));
scrollBottom();
} else {
c.unread += 1;
}
renderConvos(searchEl.value);
}, 2600);
}
function autoGrow() {
input.style.height = "auto";
input.style.height = Math.min(input.scrollHeight, 90) + "px";
}
function updateSendState() {
sendBtn.disabled = input.value.trim().length === 0;
}
// ---------- Emoji tray ----------
function buildEmojiTray() {
emojiTray.innerHTML = "";
emojis.forEach(function (e) {
var b = document.createElement("button");
b.type = "button";
b.textContent = e;
b.setAttribute("aria-label", "Insert " + e);
b.addEventListener("click", function () { insertEmoji(e); });
emojiTray.appendChild(b);
});
}
function insertEmoji(e) {
var start = input.selectionStart || input.value.length;
var end = input.selectionEnd || input.value.length;
input.value = input.value.slice(0, start) + e + input.value.slice(end);
var pos = start + e.length;
input.setSelectionRange(pos, pos);
input.focus();
autoGrow();
updateSendState();
}
function toggleTray(force) {
var open = force !== undefined ? force : emojiTray.hidden;
emojiTray.hidden = !open;
emojiTray.setAttribute("aria-hidden", open ? "false" : "true");
emojiBtn.setAttribute("aria-expanded", open ? "true" : "false");
}
// ---------- Events ----------
sendBtn.addEventListener("click", sendMessage);
input.addEventListener("input", function () { autoGrow(); updateSendState(); });
input.addEventListener("keydown", function (e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
emojiBtn.addEventListener("click", function () { toggleTray(); });
document.addEventListener("click", function (e) {
if (!emojiTray.hidden && !emojiTray.contains(e.target) && e.target !== emojiBtn) {
toggleTray(false);
}
});
searchEl.addEventListener("input", function () { renderConvos(searchEl.value); });
backBtn.addEventListener("click", function () {
phone.classList.remove("show-chat");
});
document.querySelectorAll(".chat__actions .icon-btn").forEach(function (b) {
b.addEventListener("click", function () {
toast(b.getAttribute("aria-label") + " is not available in the demo");
});
});
// ---------- Init ----------
renderNewMatches();
renderConvos("");
renderThread();
buildEmojiTray();
updateSendState();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Spark — Messages</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="phone" role="application" aria-label="Spark dating chat">
<div class="phone__notch" aria-hidden="true"></div>
<!-- Conversation list -->
<aside class="rail" id="rail" aria-label="Conversations">
<header class="rail__head">
<div class="rail__title">
<span class="logo" aria-hidden="true">✦</span>
<h1>Messages</h1>
</div>
<button class="icon-btn" type="button" aria-label="New message">✎</button>
</header>
<div class="search">
<span class="search__icon" aria-hidden="true">⌕</span>
<input type="search" id="convoSearch" placeholder="Search matches" aria-label="Search matches" />
</div>
<div class="newmatches" aria-label="New matches">
<p class="newmatches__label">New matches</p>
<div class="newmatches__row" id="newMatches"></div>
</div>
<ul class="convos" id="convos" role="listbox" aria-label="Conversation list"></ul>
</aside>
<!-- Active chat -->
<section class="chat" aria-label="Conversation">
<header class="chat__head">
<button class="icon-btn back" id="backBtn" type="button" aria-label="Back to list">‹</button>
<div class="chat__peer">
<span class="avatar avatar--sm" id="peerAvatar" aria-hidden="true"></span>
<div class="chat__peerinfo">
<p class="chat__name" id="peerName">Mia Reyes</p>
<p class="chat__status" id="peerStatus"><span class="dot dot--on"></span> Active now</p>
</div>
</div>
<div class="chat__actions">
<button class="icon-btn" type="button" aria-label="Voice call">☏</button>
<button class="icon-btn" type="button" aria-label="Video call">▶</button>
</div>
</header>
<div class="matchbanner" id="matchBanner">
<span class="matchbanner__spark" aria-hidden="true">✨</span>
<p><strong>It's a match!</strong> You and <span id="bannerName">Mia</span> liked each other on Jun 30.</p>
</div>
<div class="thread" id="thread" role="log" aria-live="polite" aria-label="Messages"></div>
<div class="typing" id="typing" hidden aria-hidden="true">
<span class="avatar avatar--xs" id="typingAvatar"></span>
<div class="typing__bubble"><span></span><span></span><span></span></div>
</div>
<footer class="composer">
<div class="emoji-tray" id="emojiTray" hidden aria-hidden="true"></div>
<div class="composer__bar">
<button class="icon-btn emoji-btn" id="emojiBtn" type="button" aria-label="Insert emoji" aria-expanded="false">☺</button>
<textarea id="msgInput" rows="1" placeholder="Message…" aria-label="Type a message"></textarea>
<button class="send-btn" id="sendBtn" type="button" aria-label="Send message">➤</button>
</div>
</footer>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A mobile-first dating chat screen presented inside a ~430px phone frame. The left rail lists conversations with gradient avatars, online rings, unread count dots and truncated last-message previews; tapping a row swaps the active thread and clears its unread badge. The header shows the matched person, their online status and quick call/video actions, with a celebratory “It’s a match!” banner sitting above the first message.
The thread renders inbound and outbound bubbles with different gradient treatments, grouped timestamps and read-receipt ticks on your own messages. Sending a message appends an outbound bubble, then shows an animated typing indicator before a contextual auto-reply drops in, keeping the illusion of a live conversation.
Everything is vanilla JS: an emoji tray toggles open and inserts characters at the caret, the input auto-grows, Enter sends while Shift+Enter adds a newline, and a small toast confirms actions. The layout collapses to a single full-width column below 520px so the list and the open chat behave like real app navigation.