Wedding — Planning Timeline
An editorial blush-and-gold wedding planner that counts down live to the big day and guides couples through a staged checklist. Days, hours, and minutes tick in real time above a milestone timeline grouped by 12-month, 6-month, and 1-month horizons. Each task checks off with a satisfying tick, the progress ring and percentage update instantly, and a filter lets you focus on what is still outstanding versus already done.
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(120% 60% at 50% -10%, var(--blush-50) 0%, transparent 60%),
var(--bg);
min-height: 100vh;
}
.wrap {
max-width: 860px;
margin: 0 auto;
padding: 48px 24px 80px;
}
/* ---------- Hero ---------- */
.hero { text-align: center; }
.eyebrow {
font-size: 0.72rem;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--gold-d);
font-weight: 600;
margin: 0 0 18px;
}
.couple {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(2.6rem, 8vw, 4.4rem);
line-height: 1.02;
margin: 0;
color: var(--ink);
}
.couple span {
display: inline-block;
color: var(--rose);
font-style: italic;
padding: 0 0.15em;
}
.venue {
font-family: var(--serif);
font-size: 1.25rem;
color: var(--ink-2);
margin: 10px 0 0;
}
/* thin gold rule flanking countdown */
.countdown {
display: flex;
align-items: flex-start;
justify-content: center;
gap: 6px;
margin: 34px auto 0;
padding: 26px 12px 10px;
position: relative;
max-width: 560px;
}
.countdown::before,
.countdown::after {
content: "";
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 90px;
height: 1px;
background: linear-gradient(90deg, transparent, var(--gold), transparent);
}
.countdown::before { top: 0; }
.countdown::after { bottom: -14px; }
.unit {
display: flex;
flex-direction: column;
align-items: center;
min-width: 72px;
}
.num {
font-family: var(--serif);
font-weight: 600;
font-size: clamp(2.2rem, 7vw, 3.4rem);
line-height: 1;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.lbl {
margin-top: 8px;
font-size: 0.62rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
.sep {
font-family: var(--serif);
font-size: 2.2rem;
color: var(--blush);
line-height: 1.1;
padding-top: 2px;
}
/* ---------- Progress card ---------- */
.progress-card {
margin-top: 46px;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 26px 30px;
display: flex;
align-items: center;
gap: 28px;
box-shadow: 0 18px 44px -30px rgba(58, 43, 43, 0.4);
}
.ring { position: relative; flex-shrink: 0; width: 120px; height: 120px; }
.ring svg { display: block; }
.ring-bg { fill: none; stroke: var(--cream); stroke-width: 9; }
.ring-fg {
fill: none;
stroke: url(#g);
stroke: var(--gold);
stroke-width: 9;
stroke-linecap: round;
stroke-dasharray: 326.72;
stroke-dashoffset: 326.72;
transition: stroke-dashoffset 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
.ring-label {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--serif);
font-weight: 600;
font-size: 2rem;
color: var(--ink);
}
.ring-label small { font-size: 0.85rem; color: var(--muted); margin-left: 1px; }
.progress-copy h2 {
font-family: var(--serif);
font-weight: 600;
font-size: 1.7rem;
margin: 0 0 4px;
}
.progress-copy p { margin: 0 0 14px; color: var(--ink-2); font-size: 0.92rem; }
.filters { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
font-family: var(--sans);
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.02em;
color: var(--ink-2);
background: var(--cream);
border: 1px solid transparent;
padding: 7px 16px;
border-radius: 999px;
cursor: pointer;
transition: background 0.2s, color 0.2s, border-color 0.2s, transform 0.1s;
}
.chip:hover { background: var(--blush-50); color: var(--rose); }
.chip:active { transform: scale(0.96); }
.chip.is-active {
background: var(--ink);
color: var(--white);
border-color: var(--ink);
}
.chip:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
/* ---------- Timeline ---------- */
.timeline { margin-top: 40px; display: flex; flex-direction: column; gap: 30px; }
.stage {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: 0 14px 40px -32px rgba(58, 43, 43, 0.4);
}
.stage[hidden] { display: none; }
.stage-head {
display: flex;
align-items: center;
gap: 16px;
padding: 22px 26px 18px;
border-bottom: 1px solid var(--line);
}
.stage-marker {
width: 46px;
height: 46px;
flex-shrink: 0;
border-radius: 50%;
display: grid;
place-items: center;
font-family: var(--serif);
font-weight: 600;
font-size: 1.1rem;
color: var(--gold-d);
background: var(--blush-50);
border: 1px solid var(--blush);
}
.stage-title { flex: 1; }
.stage-title h3 {
font-family: var(--serif);
font-weight: 600;
font-size: 1.5rem;
margin: 0;
}
.stage-title p {
margin: 2px 0 0;
font-size: 0.75rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
.stage-count {
font-size: 0.8rem;
font-weight: 700;
color: var(--gold-d);
background: var(--cream);
padding: 5px 12px;
border-radius: 999px;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.tasks { list-style: none; margin: 0; padding: 6px 0; }
.task {
display: flex;
align-items: center;
gap: 14px;
padding: 13px 26px;
cursor: pointer;
transition: background 0.18s;
}
.task[hidden] { display: none; }
.task:hover { background: var(--blush-50); }
.task:active { background: var(--cream); }
.task:focus-visible { outline: 2px solid var(--gold); outline-offset: -3px; }
.box {
width: 24px;
height: 24px;
flex-shrink: 0;
border: 1.5px solid var(--rose);
border-radius: 7px;
display: grid;
place-items: center;
transition: background 0.2s, border-color 0.2s;
}
.box svg { width: 14px; height: 14px; }
.box path {
stroke: var(--white);
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
stroke-dasharray: 24;
stroke-dashoffset: 24;
transition: stroke-dashoffset 0.35s ease 0.05s;
}
.task-text { flex: 1; font-size: 0.96rem; color: var(--ink); transition: color 0.2s; }
.task-tag {
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--rose);
background: var(--blush-50);
padding: 3px 9px;
border-radius: 999px;
white-space: nowrap;
}
.task.done .box { background: var(--gold); border-color: var(--gold); }
.task.done .box path { stroke-dashoffset: 0; }
.task.done .task-text { color: var(--muted); text-decoration: line-through; text-decoration-color: var(--blush); }
.task.done .task-tag { color: var(--muted); background: var(--cream); }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 24px);
background: var(--ink);
color: var(--white);
font-size: 0.9rem;
font-weight: 500;
padding: 12px 22px;
border-radius: 999px;
box-shadow: 0 16px 36px -14px rgba(58, 43, 43, 0.6);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s, transform 0.3s;
z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast .heart { color: var(--blush); }
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.wrap { padding: 32px 16px 64px; }
.countdown { gap: 2px; padding: 22px 4px 10px; }
.unit { min-width: 0; flex: 1; }
.sep { display: none; }
.lbl { font-size: 0.55rem; letter-spacing: 0.12em; }
.progress-card { flex-direction: column; text-align: center; gap: 18px; padding: 24px 20px; }
.filters { justify-content: center; }
.stage-head { padding: 18px 18px 14px; gap: 12px; }
.task { padding: 13px 18px; }
.task-tag { display: none; }
}(function () {
"use strict";
// Wedding date — September 12, 2026, 4:00 PM local time.
var WEDDING = new Date(2026, 8, 12, 16, 0, 0);
var STAGES = [
{
marker: "12",
title: "Twelve Months Out",
sub: "The foundation",
tasks: [
{ text: "Set the overall budget & guest count", tag: "Budget", done: true },
{ text: "Book the ceremony & reception venue", tag: "Venue", done: true },
{ text: "Reserve the wedding planner", tag: "Vendors", done: true },
{ text: "Draft the preliminary guest list", tag: "Guests", done: false },
{ text: "Book photographer & videographer", tag: "Vendors", done: false }
]
},
{
marker: "6",
title: "Six Months Out",
sub: "The details",
tasks: [
{ text: "Order the wedding dress & suits", tag: "Attire", done: true },
{ text: "Send save-the-dates", tag: "Stationery", done: false },
{ text: "Choose the florist & palette", tag: "Decor", done: false },
{ text: "Book the caterer & tasting menu", tag: "Catering", done: false },
{ text: "Reserve the honeymoon travel", tag: "Travel", done: false }
]
},
{
marker: "1",
title: "Final Month",
sub: "The countdown",
tasks: [
{ text: "Confirm final headcount with caterer", tag: "Catering", done: false },
{ text: "Finalize the seating chart", tag: "Guests", done: false },
{ text: "Pick up rings & attire fittings", tag: "Attire", done: false },
{ text: "Confirm timeline with all vendors", tag: "Vendors", done: false },
{ text: "Pack for the honeymoon", tag: "Travel", done: false }
]
}
];
var CHECK = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12.5l5 5L20 6"/></svg>';
var RING_CIRC = 2 * Math.PI * 52; // r = 52
var currentFilter = "all";
var celebrated = false;
var timelineEl = document.getElementById("timeline");
var ringEl = document.getElementById("ring");
var pctEl = document.getElementById("pct");
var progressText = document.getElementById("progressText");
var toastEl = document.getElementById("toast");
// ---------- Toast ----------
var toastTimer;
function toast(msg, heart) {
toastEl.innerHTML = heart ? msg + ' <span class="heart">♥</span>' : msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2600);
}
// ---------- Build timeline ----------
function buildTimeline() {
STAGES.forEach(function (stage, si) {
var section = document.createElement("section");
section.className = "stage";
section.dataset.stage = si;
var head = document.createElement("div");
head.className = "stage-head";
head.innerHTML =
'<div class="stage-marker" aria-hidden="true">' + stage.marker + "</div>" +
'<div class="stage-title"><h3>' + stage.title + "</h3><p>" + stage.sub + "</p></div>" +
'<div class="stage-count" data-count="' + si + '"></div>';
section.appendChild(head);
var ul = document.createElement("ul");
ul.className = "tasks";
stage.tasks.forEach(function (task, ti) {
var li = document.createElement("li");
li.className = "task" + (task.done ? " done" : "");
li.setAttribute("role", "checkbox");
li.setAttribute("tabindex", "0");
li.setAttribute("aria-checked", task.done ? "true" : "false");
li.dataset.stage = si;
li.dataset.task = ti;
li.innerHTML =
'<span class="box" aria-hidden="true">' + CHECK + "</span>" +
'<span class="task-text">' + task.text + "</span>" +
'<span class="task-tag">' + task.tag + "</span>";
li.addEventListener("click", function () { toggle(si, ti, li); });
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggle(si, ti, li);
}
});
ul.appendChild(li);
});
section.appendChild(ul);
timelineEl.appendChild(section);
});
}
// ---------- Toggle a task ----------
function toggle(si, ti, li) {
var task = STAGES[si].tasks[ti];
task.done = !task.done;
li.classList.toggle("done", task.done);
li.setAttribute("aria-checked", task.done ? "true" : "false");
update();
applyFilter();
if (task.done && allDone() && !celebrated) {
celebrated = true;
toast("Every task complete — you are ready to say I do!", true);
} else {
celebrated = celebrated && allDone();
if (task.done) toast("Nicely done", true);
}
}
function allDone() {
return STAGES.every(function (s) {
return s.tasks.every(function (t) { return t.done; });
});
}
// ---------- Progress ----------
function update() {
var total = 0, done = 0;
STAGES.forEach(function (stage, si) {
var sTotal = stage.tasks.length;
var sDone = stage.tasks.filter(function (t) { return t.done; }).length;
total += sTotal;
done += sDone;
var countEl = timelineEl.querySelector('[data-count="' + si + '"]');
if (countEl) countEl.textContent = sDone + " / " + sTotal;
});
var pct = total ? Math.round((done / total) * 100) : 0;
pctEl.textContent = pct;
ringEl.style.strokeDashoffset = RING_CIRC * (1 - pct / 100);
var remaining = total - done;
progressText.textContent = remaining === 0
? "All " + total + " tasks complete — the celebration awaits."
: done + " of " + total + " tasks complete — " + remaining + " to go.";
}
// ---------- Filter ----------
function applyFilter() {
var tasks = timelineEl.querySelectorAll(".task");
tasks.forEach(function (li) {
var isDone = li.classList.contains("done");
var show = currentFilter === "all" ||
(currentFilter === "done" && isDone) ||
(currentFilter === "todo" && !isDone);
li.hidden = !show;
});
// Hide stages that have no visible tasks.
timelineEl.querySelectorAll(".stage").forEach(function (stage) {
var anyVisible = stage.querySelector(".task:not([hidden])");
stage.hidden = !anyVisible;
});
}
document.querySelectorAll(".chip").forEach(function (chip) {
chip.addEventListener("click", function () {
document.querySelectorAll(".chip").forEach(function (c) {
c.classList.remove("is-active");
c.setAttribute("aria-pressed", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-pressed", "true");
currentFilter = chip.dataset.filter;
applyFilter();
});
});
// ---------- Countdown ----------
var els = {
days: document.getElementById("days"),
hours: document.getElementById("hours"),
mins: document.getElementById("mins"),
secs: document.getElementById("secs")
};
function pad(n, len) {
n = String(Math.max(0, n));
while (n.length < len) n = "0" + n;
return n;
}
function tick() {
var diff = WEDDING - new Date();
if (diff < 0) diff = 0;
var s = Math.floor(diff / 1000);
var days = Math.floor(s / 86400);
var hours = Math.floor((s % 86400) / 3600);
var mins = Math.floor((s % 3600) / 60);
var secs = s % 60;
els.days.textContent = pad(days, 3);
els.hours.textContent = pad(hours, 2);
els.mins.textContent = pad(mins, 2);
els.secs.textContent = pad(secs, 2);
}
// ---------- Init ----------
buildTimeline();
update();
applyFilter();
celebrated = allDone();
tick();
setInterval(tick, 1000);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wedding Planning Timeline</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">
<header class="hero">
<p class="eyebrow">Save the date · September 12, 2026</p>
<h1 class="couple">Amara <span>&</span> Julian</h1>
<p class="venue">Rosewood Orchard · Sonoma, California</p>
<div class="countdown" id="countdown" role="timer" aria-live="polite" aria-label="Time remaining until the wedding">
<div class="unit"><span class="num" id="days">000</span><span class="lbl">Days</span></div>
<div class="sep" aria-hidden="true">:</div>
<div class="unit"><span class="num" id="hours">00</span><span class="lbl">Hours</span></div>
<div class="sep" aria-hidden="true">:</div>
<div class="unit"><span class="num" id="mins">00</span><span class="lbl">Minutes</span></div>
<div class="sep" aria-hidden="true">:</div>
<div class="unit"><span class="num" id="secs">00</span><span class="lbl">Seconds</span></div>
</div>
</header>
<section class="progress-card" aria-label="Planning progress">
<div class="ring" role="img" aria-label="Overall planning progress">
<svg viewBox="0 0 120 120" width="120" height="120">
<circle class="ring-bg" cx="60" cy="60" r="52" />
<circle class="ring-fg" id="ring" cx="60" cy="60" r="52" transform="rotate(-90 60 60)" />
</svg>
<div class="ring-label"><span id="pct">0</span><small>%</small></div>
</div>
<div class="progress-copy">
<h2>Planning progress</h2>
<p id="progressText">0 of 0 tasks complete — let’s begin the journey.</p>
<div class="filters" role="group" aria-label="Filter tasks">
<button class="chip is-active" data-filter="all" aria-pressed="true">All</button>
<button class="chip" data-filter="todo" aria-pressed="false">To do</button>
<button class="chip" data-filter="done" aria-pressed="false">Completed</button>
</div>
</div>
</section>
<section class="timeline" id="timeline" aria-label="Planning timeline"></section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A romantic, self-contained wedding planning dashboard. A live countdown header renders the days, hours, minutes, and seconds remaining until the wedding date, refreshed every second, framed with thin gold rules and a Cormorant Garamond display face. Beneath it, a circular progress ring and a running percentage summarize how much of the checklist is complete.
The body is a staged timeline split into three planning horizons — twelve months out, six months out, and the final month. Each stage lists realistic tasks (book the venue, order the dress, finalize the seating chart) as check-off rows with hover, active, and keyboard-focus states. Ticking a box animates the checkmark, updates the progress ring and per-stage counts, and reveals a celebratory toast when everything is done.
A small filter bar lets couples switch between all tasks, only the outstanding ones, and completed work, so the list stays focused as the date approaches. State is kept in memory and every control is fully keyboard-operable with visible focus and accessible labels.