Wedding — Guest List
An editorial blush-and-gold guest list manager for couples planning the big day. Track every invitee in a serif-styled table with RSVP status chips for confirmed, pending, and declined replies. Add new guests inline, filter the roster by status, and watch the live headcount tallies update as answers roll in. Elegant, airy, and thoughtfully accessible from desktop down to a 360px phone.
MCP
Code
:root {
--blush: #e8b7b0;
--blush-50: #fbeeec;
--rose: #c98a86;
--gold: #c9a24b;
--gold-d: #a8862f;
--ink: #3a2b2b;
--ink-2: #6b5555;
--muted: #9a8585;
--bg: #fbf7f4;
--cream: #f4ece4;
--white: #fff;
--line: rgba(58, 43, 43, 0.12);
--r-sm: 8px;
--r-md: 16px;
--r-lg: 26px;
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
}
* {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
font-family: var(--sans);
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1200px 500px at 80% -10%, var(--blush-50), transparent 60%),
var(--bg);
min-height: 100vh;
padding: clamp(1.25rem, 4vw, 3rem) 1rem 4rem;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.wrap {
max-width: 940px;
margin: 0 auto;
}
/* ---------- masthead ---------- */
.masthead {
text-align: center;
padding-bottom: 1.75rem;
margin-bottom: 2rem;
position: relative;
}
.masthead::after {
content: "";
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
width: 120px;
height: 1px;
background: linear-gradient(90deg, transparent, var(--gold), transparent);
}
.eyebrow {
margin: 0 0 0.4rem;
font-size: 0.72rem;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--gold-d);
font-weight: 600;
}
.masthead h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(2.4rem, 7vw, 3.6rem);
line-height: 1.05;
margin: 0.1rem 0 0.5rem;
color: var(--ink);
}
.sub {
margin: 0;
color: var(--ink-2);
font-size: 0.98rem;
font-style: italic;
font-family: var(--serif);
font-size: 1.15rem;
}
/* ---------- stats ---------- */
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.85rem;
margin-bottom: 2.25rem;
}
.stat {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 1.1rem 0.75rem;
text-align: center;
box-shadow: 0 10px 30px -24px rgba(58, 43, 43, 0.5);
position: relative;
overflow: hidden;
}
.stat::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 3px;
background: var(--gold);
opacity: 0.55;
}
.stat.is-confirmed::before { background: #6f9a6a; opacity: 0.75; }
.stat.is-pending::before { background: var(--gold); opacity: 0.85; }
.stat.is-declined::before { background: var(--rose); opacity: 0.8; }
.stat-num {
display: block;
font-family: var(--serif);
font-weight: 600;
font-size: 2.4rem;
line-height: 1;
color: var(--ink);
}
.stat-label {
display: block;
margin-top: 0.35rem;
font-size: 0.72rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
/* ---------- panels ---------- */
.panel {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 1.5rem;
margin-bottom: 1.75rem;
box-shadow: 0 20px 50px -38px rgba(58, 43, 43, 0.55);
}
.panel-title {
font-family: var(--serif);
font-weight: 600;
font-size: 1.5rem;
margin: 0 0 1.1rem;
color: var(--ink);
}
/* ---------- add form ---------- */
.add-form {
display: grid;
grid-template-columns: 2fr 1fr 1.6fr auto;
gap: 0.85rem;
align-items: end;
}
.field {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.field label {
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
.field input,
.field select {
font-family: var(--sans);
font-size: 0.94rem;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 0.62rem 0.7rem;
transition: border-color 0.18s, box-shadow 0.18s;
width: 100%;
}
.field input::placeholder { color: #bda9a9; }
.field input:focus,
.field select:focus {
outline: none;
border-color: var(--rose);
box-shadow: 0 0 0 3px rgba(201, 138, 134, 0.28);
}
.btn-add {
font-family: var(--sans);
font-weight: 600;
font-size: 0.9rem;
letter-spacing: 0.02em;
color: var(--white);
background: linear-gradient(180deg, var(--rose), #b9736f);
border: none;
border-radius: var(--r-sm);
padding: 0.68rem 1.25rem;
cursor: pointer;
white-space: nowrap;
transition: transform 0.12s, box-shadow 0.2s, filter 0.2s;
box-shadow: 0 8px 20px -10px rgba(185, 115, 111, 0.7);
}
.btn-add:hover { filter: brightness(1.05); box-shadow: 0 12px 24px -10px rgba(185, 115, 111, 0.8); }
.btn-add:active { transform: translateY(1px); }
.btn-add:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(201, 162, 75, 0.5);
}
/* ---------- toolbar / filters ---------- */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.toolbar .panel-title { margin: 0; }
.filters {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.chip-filter {
font-family: var(--sans);
font-size: 0.8rem;
font-weight: 600;
color: var(--ink-2);
background: var(--cream);
border: 1px solid var(--line);
border-radius: 999px;
padding: 0.4rem 0.9rem;
cursor: pointer;
transition: background 0.18s, color 0.18s, border-color 0.18s;
}
.chip-filter:hover { border-color: var(--gold); color: var(--ink); }
.chip-filter.is-active {
background: var(--ink);
color: var(--white);
border-color: var(--ink);
}
.chip-filter:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(201, 162, 75, 0.5);
}
/* ---------- table ---------- */
.table-scroll { overflow-x: auto; }
.guest-table {
width: 100%;
border-collapse: collapse;
min-width: 520px;
}
.guest-table thead th {
text-align: left;
font-size: 0.7rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
font-weight: 700;
padding: 0 0.75rem 0.7rem;
border-bottom: 1px solid var(--gold);
}
.guest-table th.num,
.guest-table td.num { text-align: center; }
.guest-table tbody tr {
border-bottom: 1px solid var(--line);
transition: background 0.15s;
}
.guest-table tbody tr:hover { background: var(--blush-50); }
.guest-table tbody tr:last-child { border-bottom: none; }
.guest-table td {
padding: 0.85rem 0.75rem;
font-size: 0.94rem;
color: var(--ink);
vertical-align: middle;
}
.g-name {
display: flex;
align-items: center;
gap: 0.7rem;
}
.g-avatar {
width: 34px;
height: 34px;
border-radius: 50%;
display: grid;
place-items: center;
font-family: var(--serif);
font-weight: 600;
font-size: 0.95rem;
color: var(--gold-d);
background: var(--blush-50);
border: 1px solid rgba(201, 162, 75, 0.35);
flex: none;
}
.g-name b { font-weight: 600; }
.g-group { color: var(--ink-2); font-size: 0.88rem; }
/* status chip */
.chip {
font-family: var(--sans);
font-size: 0.76rem;
font-weight: 600;
letter-spacing: 0.02em;
border-radius: 999px;
padding: 0.32rem 0.72rem 0.32rem 0.62rem;
border: 1px solid transparent;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.4rem;
transition: transform 0.12s, filter 0.15s;
}
.chip::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
}
.chip:hover { filter: brightness(0.97); }
.chip:active { transform: scale(0.96); }
.chip:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(201, 162, 75, 0.45);
}
.chip.confirmed { color: #3f6b3a; background: #e7f0e4; border-color: #c3ddbb; }
.chip.pending { color: var(--gold-d); background: #f8efd8; border-color: #ecd9a6; }
.chip.declined { color: #a1504c; background: #f7e3e1; border-color: #eec4c0; }
/* remove button */
.g-remove {
background: none;
border: 1px solid var(--line);
border-radius: var(--r-sm);
width: 30px;
height: 30px;
color: var(--muted);
cursor: pointer;
font-size: 1rem;
line-height: 1;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.g-remove:hover { color: var(--rose); border-color: var(--rose); background: var(--blush-50); }
.g-remove:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(201, 138, 134, 0.4);
}
.empty {
text-align: center;
color: var(--muted);
font-family: var(--serif);
font-size: 1.15rem;
padding: 2rem 1rem;
margin: 0;
}
.hint {
margin: 1rem 0 0;
font-size: 0.8rem;
color: var(--muted);
font-style: italic;
}
/* row enter animation */
@keyframes rowIn {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
.row-in { animation: rowIn 0.28s ease; }
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 1.5rem;
transform: translate(-50%, 1.5rem);
background: var(--ink);
color: var(--white);
font-size: 0.88rem;
font-weight: 500;
padding: 0.7rem 1.25rem;
border-radius: 999px;
box-shadow: 0 16px 40px -16px rgba(58, 43, 43, 0.7);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast::before {
content: "✦ ";
color: var(--gold);
}
/* ---------- responsive ---------- */
@media (max-width: 520px) {
.stats {
grid-template-columns: repeat(2, 1fr);
}
.add-form {
grid-template-columns: 1fr 1fr;
}
.field-name { grid-column: 1 / -1; }
.btn-add { grid-column: 1 / -1; }
.guest-table { min-width: 0; }
.guest-table thead { display: none; }
.guest-table,
.guest-table tbody,
.guest-table tr,
.guest-table td {
display: block;
width: 100%;
}
.guest-table tbody tr {
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 0.6rem 0.9rem;
margin-bottom: 0.85rem;
}
.guest-table tbody tr:hover { background: var(--blush-50); }
.guest-table td {
padding: 0.4rem 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.guest-table td.num { justify-content: space-between; }
.guest-table td::before {
content: attr(data-label);
font-size: 0.68rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
font-weight: 700;
}
.guest-table td.cell-name::before { display: none; }
.g-name { flex: 1; }
}(function () {
"use strict";
var STATUSES = ["confirmed", "pending", "declined"];
var STATUS_LABEL = {
confirmed: "Confirmed",
pending: "Pending",
declined: "Declined"
};
var guests = [
{ id: 1, name: "Eleanor Whitmore", group: "Family of the Bride", party: 2, status: "confirmed" },
{ id: 2, name: "Theodore Ashby", group: "Family of the Groom", party: 1, status: "confirmed" },
{ id: 3, name: "Marguerite Vance", group: "College Friends", party: 2, status: "pending" },
{ id: 4, name: "Rupert Callahan", group: "Work & Colleagues", party: 1, status: "declined" },
{ id: 5, name: "Priya Sundaram", group: "College Friends", party: 2, status: "confirmed" },
{ id: 6, name: "Nathaniel Brooks", group: "Neighbours", party: 4, status: "pending" },
{ id: 7, name: "Clementine Rhodes", group: "Family of the Bride", party: 1, status: "confirmed" },
{ id: 8, name: "Oscar Delacroix", group: "Work & Colleagues", party: 2, status: "declined" }
];
var nextId = 9;
var currentFilter = "all";
var body = document.getElementById("guestBody");
var emptyState = document.getElementById("emptyState");
var form = document.getElementById("addForm");
var toastEl = document.getElementById("toast");
var filterBtns = Array.prototype.slice.call(document.querySelectorAll(".chip-filter"));
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2400);
}
function initials(name) {
var parts = name.trim().split(/\s+/);
var first = parts[0] ? parts[0][0] : "";
var last = parts.length > 1 ? parts[parts.length - 1][0] : "";
return (first + last).toUpperCase();
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
}
function updateStats() {
var counts = { total: 0, confirmed: 0, pending: 0, declined: 0 };
guests.forEach(function (g) {
counts.total += 1;
counts[g.status] += 1;
});
Object.keys(counts).forEach(function (key) {
var el = document.querySelector('[data-count="' + key + '"]');
if (el) el.textContent = counts[key];
});
}
function rowHtml(g, isNew) {
return (
'<tr data-id="' + g.id + '"' + (isNew ? ' class="row-in"' : "") + ">" +
'<td class="cell-name" data-label="Guest">' +
'<span class="g-name">' +
'<span class="g-avatar" aria-hidden="true">' + escapeHtml(initials(g.name)) + "</span>" +
"<b>" + escapeHtml(g.name) + "</b>" +
"</span>" +
"</td>" +
'<td class="g-group" data-label="Group">' + escapeHtml(g.group) + "</td>" +
'<td class="num" data-label="Party">' + g.party + "</td>" +
'<td data-label="RSVP">' +
'<button class="chip ' + g.status + '" data-action="cycle" ' +
'aria-label="RSVP status: ' + STATUS_LABEL[g.status] + '. Click to change.">' +
STATUS_LABEL[g.status] +
"</button>" +
"</td>" +
'<td data-label="Remove">' +
'<button class="g-remove" data-action="remove" ' +
'aria-label="Remove ' + escapeHtml(g.name) + ' from list" title="Remove">×</button>' +
"</td>" +
"</tr>"
);
}
function render(newId) {
var visible = guests.filter(function (g) {
return currentFilter === "all" || g.status === currentFilter;
});
body.innerHTML = visible
.map(function (g) {
return rowHtml(g, g.id === newId);
})
.join("");
emptyState.hidden = visible.length !== 0;
updateStats();
}
// Add guest
form.addEventListener("submit", function (e) {
e.preventDefault();
var data = new FormData(form);
var name = (data.get("name") || "").toString().trim();
if (!name) {
toast("Please enter a guest name.");
return;
}
var g = {
id: nextId++,
name: name,
group: (data.get("group") || "").toString(),
party: parseInt(data.get("party"), 10) || 1,
status: "pending"
};
guests.push(g);
// if a filter would hide the new guest, jump back to All so it's visible
if (currentFilter !== "all" && currentFilter !== "pending") {
setFilter("all");
}
render(g.id);
form.reset();
document.getElementById("guestName").focus();
toast(name + " added — awaiting reply.");
});
// Delegated actions: cycle status + remove
body.addEventListener("click", function (e) {
var actionEl = e.target.closest("[data-action]");
if (!actionEl) return;
var row = e.target.closest("tr");
if (!row) return;
var id = parseInt(row.getAttribute("data-id"), 10);
var guest = guests.find(function (g) {
return g.id === id;
});
if (!guest) return;
if (actionEl.getAttribute("data-action") === "cycle") {
var idx = STATUSES.indexOf(guest.status);
guest.status = STATUSES[(idx + 1) % STATUSES.length];
render();
toast(guest.name + " · " + STATUS_LABEL[guest.status]);
} else if (actionEl.getAttribute("data-action") === "remove") {
guests = guests.filter(function (g) {
return g.id !== id;
});
render();
toast(guest.name + " removed from the list.");
}
});
// Filters
function setFilter(filter) {
currentFilter = filter;
filterBtns.forEach(function (btn) {
var active = btn.getAttribute("data-filter") === filter;
btn.classList.toggle("is-active", active);
btn.setAttribute("aria-pressed", active ? "true" : "false");
});
render();
}
filterBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
setFilter(btn.getAttribute("data-filter"));
});
});
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wedding — Guest List</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=Cormorant+Garamond:wght@400;500;600&family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap" role="main">
<header class="masthead">
<p class="eyebrow">Amelia & Julian · June 2026</p>
<h1>The Guest List</h1>
<p class="sub">A quiet ledger of everyone we cannot wait to celebrate with.</p>
</header>
<section class="stats" aria-label="Guest count summary">
<article class="stat">
<span class="stat-num" data-count="total">0</span>
<span class="stat-label">Invited</span>
</article>
<article class="stat is-confirmed">
<span class="stat-num" data-count="confirmed">0</span>
<span class="stat-label">Confirmed</span>
</article>
<article class="stat is-pending">
<span class="stat-num" data-count="pending">0</span>
<span class="stat-label">Pending</span>
</article>
<article class="stat is-declined">
<span class="stat-num" data-count="declined">0</span>
<span class="stat-label">Declined</span>
</article>
</section>
<section class="panel" aria-label="Add a guest">
<h2 class="panel-title">Add a guest</h2>
<form class="add-form" id="addForm" autocomplete="off">
<div class="field field-name">
<label for="guestName">Name</label>
<input id="guestName" name="name" type="text" placeholder="e.g. Rosalind Hart" required />
</div>
<div class="field">
<label for="guestParty">Party</label>
<select id="guestParty" name="party">
<option value="1">1 seat</option>
<option value="2">2 seats</option>
<option value="3">3 seats</option>
<option value="4">4 seats</option>
</select>
</div>
<div class="field">
<label for="guestGroup">Group</label>
<select id="guestGroup" name="group">
<option>Family of the Bride</option>
<option>Family of the Groom</option>
<option>College Friends</option>
<option>Work & Colleagues</option>
<option>Neighbours</option>
</select>
</div>
<button class="btn-add" type="submit">Add to list</button>
</form>
</section>
<section class="panel" aria-label="Guest roster">
<div class="toolbar">
<h2 class="panel-title">Roster</h2>
<div class="filters" role="group" aria-label="Filter guests by RSVP status">
<button class="chip-filter is-active" data-filter="all" aria-pressed="true">All</button>
<button class="chip-filter" data-filter="confirmed" aria-pressed="false">Confirmed</button>
<button class="chip-filter" data-filter="pending" aria-pressed="false">Pending</button>
<button class="chip-filter" data-filter="declined" aria-pressed="false">Declined</button>
</div>
</div>
<div class="table-scroll">
<table class="guest-table">
<caption class="sr-only">Wedding guests with party size, seating group and RSVP status</caption>
<thead>
<tr>
<th scope="col">Guest</th>
<th scope="col">Group</th>
<th scope="col" class="num">Party</th>
<th scope="col">RSVP</th>
<th scope="col"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody id="guestBody"><!-- rows injected by script.js --></tbody>
</table>
<p class="empty" id="emptyState" hidden>No guests in this category yet.</p>
</div>
<p class="hint">Tip: tap any RSVP chip to cycle Confirmed → Pending → Declined.</p>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A romantic, editorial guest list manager for wedding planning. The screen opens with a stat row of gold-rimmed count cards — total invited, confirmed, pending, and declined — that recalculate the moment any reply changes. Beneath it sits a serif-headed table listing each guest with their party size, seating group, and a colour-coded RSVP chip. Every row carries hover and focus states with thin gold rules separating the entries.
Adding a guest happens through an inline form: type a name, choose a party size and group, and the new invitee slides into the table as a pending reply while a soft toast confirms the addition. Clicking any status chip cycles it through confirmed, pending, and declined, so you can update answers without leaving the roster.
A filter bar of pill buttons narrows the view to a single RSVP status, updating both the visible rows and an “empty state” message when a category has no guests. The whole layout is keyboard-usable with visible focus rings, meets WCAG AA contrast against the cream background, and reflows into a stacked card layout below 520px so it stays legible on a phone.