Wedding — Budget Tracker
An editorial blush-and-gold wedding budget tracker with category rows comparing estimated versus actual spend, an add-expense form, a live total-spent progress bar, and a gentle over-budget warning that softly alerts couples when a category or the grand total drifts past plan.
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 50% -10%, var(--blush-50), transparent 70%),
var(--bg);
padding: 40px 20px 72px;
}
.wrap {
max-width: 880px;
margin: 0 auto;
}
/* Masthead */
.masthead {
text-align: center;
padding-bottom: 26px;
border-bottom: 1px solid var(--gold);
position: relative;
}
.masthead::after {
content: "";
position: absolute;
left: 50%;
bottom: -4px;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--gold);
transform: translateX(-50%);
}
.eyebrow {
margin: 0 0 6px;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--rose);
font-weight: 600;
}
.masthead h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(2.6rem, 7vw, 3.8rem);
line-height: 1.02;
margin: 4px 0 10px;
letter-spacing: 0.01em;
}
.lede {
margin: 0;
color: var(--ink-2);
font-family: var(--serif);
font-size: 1.15rem;
font-style: italic;
}
/* Topline */
.topline {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 26px 28px 24px;
margin: 30px 0;
box-shadow: 0 18px 40px -28px rgba(58, 43, 43, 0.4);
}
.topline-head {
display: flex;
justify-content: space-between;
gap: 20px;
align-items: flex-end;
margin-bottom: 18px;
}
.topline-label {
display: block;
font-size: 0.7rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
margin-bottom: 4px;
}
.topline-right {
text-align: right;
}
.budget-edit {
display: inline-flex;
align-items: baseline;
gap: 2px;
font-family: var(--serif);
}
.currency {
color: var(--gold-d);
font-family: var(--serif);
font-size: 1.6rem;
font-weight: 600;
}
.budget-input {
font-family: var(--serif);
font-size: 2.1rem;
font-weight: 600;
color: var(--ink);
border: none;
border-bottom: 1px dashed var(--line);
background: transparent;
width: 150px;
padding: 0 2px 2px;
}
.budget-input:focus {
outline: none;
border-bottom-color: var(--gold);
}
.spent-figure {
font-family: var(--serif);
font-size: 2.1rem;
font-weight: 600;
color: var(--rose);
}
.bar-track {
height: 14px;
border-radius: 999px;
background: var(--cream);
border: 1px solid var(--line);
overflow: hidden;
position: relative;
}
.bar-fill {
height: 100%;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, var(--blush), var(--rose));
transition: width 0.55s cubic-bezier(0.22, 1, 0.36, 1), background 0.4s ease;
}
.bar-track.over .bar-fill {
background: linear-gradient(90deg, var(--rose), var(--gold-d));
}
.bar-legend {
display: flex;
justify-content: space-between;
font-size: 0.82rem;
margin-top: 10px;
color: var(--ink-2);
}
.remain-good {
color: var(--gold-d);
font-weight: 600;
}
.remain-bad {
color: #b0453f;
font-weight: 700;
}
.warn {
display: flex;
align-items: center;
gap: 9px;
margin: 16px 0 0;
padding: 11px 15px;
border-radius: var(--r-sm);
background: #fdf1ec;
border: 1px solid var(--blush);
color: #a3433d;
font-size: 0.88rem;
font-weight: 500;
}
.warn-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--rose);
flex: none;
animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.5; }
}
/* Ledger */
.ledger {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: 0 18px 40px -32px rgba(58, 43, 43, 0.4);
}
.ledger-head,
.row {
display: grid;
grid-template-columns: 1.6fr 1fr 1fr 1fr 0.9fr;
gap: 12px;
align-items: center;
padding: 14px 24px;
}
.ledger-head {
background: var(--cream);
font-size: 0.68rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--muted);
font-weight: 700;
border-bottom: 1px solid var(--line);
}
.ledger-head .num,
.row .num {
text-align: right;
}
.ledger-head .stat,
.row .stat {
text-align: center;
}
.rows {
list-style: none;
margin: 0;
padding: 0;
}
.row {
border-bottom: 1px solid var(--line);
transition: background 0.2s ease;
}
.row:last-child {
border-bottom: none;
}
.row:hover {
background: var(--blush-50);
}
.cat-name {
display: flex;
align-items: center;
gap: 11px;
font-weight: 600;
font-size: 0.96rem;
}
.cat-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex: none;
box-shadow: 0 0 0 3px rgba(201, 162, 75, 0.14);
}
.cat-sub {
display: block;
font-size: 0.72rem;
color: var(--muted);
font-weight: 400;
margin-top: 1px;
}
.num {
font-variant-numeric: tabular-nums;
font-size: 0.94rem;
}
.num.est {
color: var(--ink-2);
}
.num.act {
font-weight: 700;
}
.num.rem-good {
color: var(--gold-d);
}
.num.rem-bad {
color: #b0453f;
}
.badge {
display: inline-block;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 4px 9px;
border-radius: 999px;
}
.badge.ok {
background: rgba(201, 162, 75, 0.12);
color: var(--gold-d);
}
.badge.warn-b {
background: #fbe3dd;
color: #b0453f;
}
.badge.empty {
background: var(--cream);
color: var(--muted);
}
/* Add card */
.addcard {
background: linear-gradient(180deg, var(--white), var(--blush-50));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 24px 28px 26px;
margin: 28px 0;
}
.addcard h2 {
font-family: var(--serif);
font-weight: 600;
font-size: 1.7rem;
margin: 0 0 16px;
}
.expense-form {
display: flex;
flex-wrap: wrap;
gap: 14px;
align-items: flex-end;
}
.field {
display: flex;
flex-direction: column;
gap: 5px;
}
.field.grow {
flex: 1 1 180px;
}
.field label {
font-size: 0.68rem;
letter-spacing: 0.13em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
.field select,
.field input,
.amount-wrap {
font-family: var(--sans);
font-size: 0.95rem;
color: var(--ink);
padding: 11px 13px;
border: 1px solid var(--line);
border-radius: var(--r-sm);
background: var(--white);
}
.field select {
min-width: 150px;
cursor: pointer;
}
.amount-wrap {
display: flex;
align-items: center;
gap: 4px;
padding: 0 13px;
width: 130px;
}
.amount-wrap .currency {
font-family: var(--sans);
font-size: 1rem;
color: var(--gold-d);
}
.amount-wrap input {
border: none;
padding: 11px 0;
width: 100%;
background: transparent;
}
.field select:focus,
.field input:focus,
.amount-wrap:focus-within {
outline: none;
border-color: var(--gold);
box-shadow: 0 0 0 3px rgba(201, 162, 75, 0.2);
}
.amount-wrap:focus-within input:focus {
box-shadow: none;
}
.btn-add {
font-family: var(--sans);
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 0.02em;
color: var(--white);
background: linear-gradient(135deg, var(--rose), var(--gold-d));
border: none;
padding: 12px 24px;
border-radius: 999px;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.2s ease, filter 0.2s ease;
box-shadow: 0 8px 18px -8px rgba(168, 134, 47, 0.7);
}
.btn-add:hover {
transform: translateY(-2px);
filter: brightness(1.05);
}
.btn-add:active {
transform: translateY(0);
}
.btn-add:focus-visible {
outline: 2px solid var(--gold-d);
outline-offset: 3px;
}
/* Summary */
.summary {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.tile {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px 20px;
text-align: center;
}
.tile-label {
display: block;
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
margin-bottom: 6px;
}
.tile-val {
font-family: var(--serif);
font-size: 1.75rem;
font-weight: 600;
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 24px);
background: var(--ink);
color: var(--white);
padding: 13px 22px;
border-radius: 999px;
font-size: 0.88rem;
font-weight: 500;
box-shadow: 0 14px 34px -12px rgba(58, 43, 43, 0.6);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s ease;
z-index: 50;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
.toast::before {
content: "\2665";
color: var(--blush);
margin-right: 8px;
}
@media (max-width: 520px) {
body {
padding: 26px 14px 60px;
}
.topline,
.addcard {
padding: 20px 18px;
}
.topline-head {
flex-direction: column;
align-items: flex-start;
gap: 14px;
}
.topline-right {
text-align: left;
}
.ledger-head {
display: none;
}
.row {
grid-template-columns: 1fr 1fr;
gap: 8px 12px;
padding: 16px 18px;
}
.row .cat-name {
grid-column: 1 / -1;
}
.num,
.row .stat {
text-align: left;
}
.num::before {
content: attr(data-label);
display: block;
font-size: 0.6rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
font-weight: 700;
margin-bottom: 2px;
}
.row .stat {
grid-column: 1 / -1;
}
.expense-form {
flex-direction: column;
align-items: stretch;
}
.field,
.field.grow,
.amount-wrap {
width: 100%;
}
.btn-add {
width: 100%;
}
.summary {
grid-template-columns: 1fr;
}
}(function () {
"use strict";
// --- Model -----------------------------------------------------------------
var categories = [
{ id: "venue", name: "Venue & Rentals", note: "Vineyard hall, tables, tent", color: "#c98a86", estimated: 14000, actual: 12500 },
{ id: "catering", name: "Catering & Bar", note: "Dinner, drinks, cake", color: "#c9a24b", estimated: 11000, actual: 11800 },
{ id: "photo", name: "Photo & Video", note: "Full-day coverage", color: "#a8862f", estimated: 4500, actual: 4500 },
{ id: "florals", name: "Florals & Decor", note: "Bouquets, arch, centerpieces", color: "#e8b7b0", estimated: 3800, actual: 2100 },
{ id: "attire", name: "Attire & Beauty", note: "Dress, suit, hair, makeup", color: "#6b5555", estimated: 3200, actual: 2950 },
{ id: "music", name: "Music & Entertainment", note: "Band and DJ", color: "#9a8585", estimated: 2500, actual: 0 },
{ id: "stationery", name: "Stationery & Favors", note: "Invites, signage, gifts", color: "#c9a24b", estimated: 1600, actual: 640 },
];
var budget = 42000;
// --- Helpers ---------------------------------------------------------------
var fmt = function (n) {
var neg = n < 0;
var s = "$" + Math.abs(Math.round(n)).toLocaleString("en-US");
return neg ? "-" + s : s;
};
var $ = function (id) { return document.getElementById(id); };
var toastEl = $("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2600);
}
// --- Render categories -----------------------------------------------------
var rowsEl = $("rows");
var catSelect = $("catSelect");
function buildSelect() {
catSelect.innerHTML = "";
categories.forEach(function (c) {
var opt = document.createElement("option");
opt.value = c.id;
opt.textContent = c.name;
catSelect.appendChild(opt);
});
}
function renderRows() {
rowsEl.innerHTML = "";
categories.forEach(function (c) {
var remaining = c.estimated - c.actual;
var over = c.actual > c.estimated;
var li = document.createElement("li");
li.className = "row";
li.dataset.id = c.id;
var status;
if (c.actual === 0) {
status = '<span class="badge empty">Unbooked</span>';
} else if (over) {
status = '<span class="badge warn-b">Over</span>';
} else {
status = '<span class="badge ok">On track</span>';
}
var remClass = over ? "rem-bad" : "rem-good";
li.innerHTML =
'<div class="cat-name"><span class="cat-dot" style="background:' + c.color + '"></span>' +
'<span>' + c.name + '<span class="cat-sub">' + c.note + '</span></span></div>' +
'<span class="num est" data-label="Estimated">' + fmt(c.estimated) + '</span>' +
'<span class="num act" data-label="Actual">' + fmt(c.actual) + '</span>' +
'<span class="num ' + remClass + '" data-label="Remaining">' + fmt(remaining) + '</span>' +
'<span class="stat">' + status + '</span>';
rowsEl.appendChild(li);
});
}
// --- Totals + progress bar -------------------------------------------------
var barTrack = $("barTrack");
var barFill = $("barFill");
function updateTotals() {
var estTotal = 0, spent = 0;
categories.forEach(function (c) {
estTotal += c.estimated;
spent += c.actual;
});
var remaining = budget - spent;
var pct = budget > 0 ? (spent / budget) * 100 : 0;
var over = spent > budget;
barFill.style.width = Math.min(pct, 100) + "%";
barTrack.classList.toggle("over", over);
barTrack.setAttribute("aria-valuenow", Math.round(pct));
$("pctLabel").textContent = Math.round(pct) + "% of budget used";
$("spentTotal").textContent = fmt(spent);
$("estTotal").textContent = fmt(estTotal);
$("spentTile").textContent = fmt(spent);
$("remainTile").textContent = fmt(remaining);
var remainLabel = $("remainLabel");
if (over) {
remainLabel.textContent = fmt(Math.abs(remaining)) + " over budget";
remainLabel.className = "remain-bad";
} else {
remainLabel.textContent = fmt(remaining) + " remaining";
remainLabel.className = "remain-good";
}
var remTile = $("remainTile");
remTile.style.color = over ? "#b0453f" : "";
// Warning banner: overall over budget OR any category over.
var overCats = categories.filter(function (c) { return c.actual > c.estimated; });
var warn = $("warnBanner");
var warnText = $("warnText");
if (over) {
warnText.textContent = "Heads up — you are " + fmt(Math.abs(remaining)) + " over your total budget.";
warn.hidden = false;
} else if (overCats.length) {
var names = overCats.map(function (c) { return c.name; }).join(", ");
warnText.textContent = overCats.length === 1
? names + " is over its estimate."
: names + " are over their estimates.";
warn.hidden = false;
} else {
warn.hidden = true;
}
}
// --- Add expense -----------------------------------------------------------
var form = $("expenseForm");
var itemInput = $("itemInput");
var amountInput = $("amountInput");
form.addEventListener("submit", function (e) {
e.preventDefault();
var catId = catSelect.value;
var item = itemInput.value.trim();
var amount = parseFloat(amountInput.value);
if (!item) {
toast("Name the line item first");
itemInput.focus();
return;
}
if (!(amount > 0)) {
toast("Enter an amount above zero");
amountInput.focus();
return;
}
var cat = categories.find(function (c) { return c.id === catId; });
if (!cat) return;
cat.actual += amount;
renderRows();
updateTotals();
// Flash the affected row.
var row = rowsEl.querySelector('.row[data-id="' + catId + '"]');
if (row) {
row.style.background = "var(--blush-50)";
row.animate(
[{ background: "#f6ddd6" }, { background: "rgba(251,238,236,0)" }],
{ duration: 900, easing: "ease-out" }
);
}
toast(fmt(amount) + " logged to " + cat.name);
form.reset();
catSelect.value = catId;
itemInput.focus();
});
// --- Editable budget -------------------------------------------------------
$("budgetInput").addEventListener("input", function (e) {
var v = parseFloat(e.target.value);
budget = isNaN(v) || v < 0 ? 0 : v;
updateTotals();
});
// --- Init ------------------------------------------------------------------
buildSelect();
renderRows();
updateTotals();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wedding Budget Tracker</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="masthead">
<p class="eyebrow">Amelia & Julian · October Vineyard Wedding</p>
<h1>The Budget Ledger</h1>
<p class="lede">A quiet place to weigh what you dreamed against what you spent.</p>
</header>
<section class="topline" aria-label="Overall budget">
<div class="topline-head">
<div>
<span class="topline-label">Total budget</span>
<div class="budget-edit">
<span class="currency">$</span>
<input
id="budgetInput"
class="budget-input"
type="number"
min="0"
step="500"
value="42000"
aria-label="Total wedding budget"
/>
</div>
</div>
<div class="topline-right">
<span class="topline-label">Spent so far</span>
<div class="spent-figure"><span id="spentTotal">$0</span></div>
</div>
</div>
<div class="bar-track" role="progressbar" aria-label="Budget used" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" id="barTrack">
<div class="bar-fill" id="barFill"></div>
</div>
<div class="bar-legend">
<span id="pctLabel">0% of budget used</span>
<span id="remainLabel" class="remain-good">$42,000 remaining</span>
</div>
<p class="warn" id="warnBanner" hidden>
<span class="warn-dot" aria-hidden="true"></span>
<span id="warnText">You are over budget.</span>
</p>
</section>
<section class="ledger" aria-label="Budget categories">
<div class="ledger-head" aria-hidden="true">
<span>Category</span>
<span class="num">Estimated</span>
<span class="num">Actual</span>
<span class="num">Remaining</span>
<span class="stat">Status</span>
</div>
<ul class="rows" id="rows"></ul>
</section>
<section class="addcard" aria-label="Add an expense">
<h2>Log an expense</h2>
<form id="expenseForm" class="expense-form" novalidate>
<div class="field">
<label for="catSelect">Category</label>
<select id="catSelect" required></select>
</div>
<div class="field grow">
<label for="itemInput">Line item</label>
<input id="itemInput" type="text" placeholder="Florist deposit" required />
</div>
<div class="field">
<label for="amountInput">Amount</label>
<div class="amount-wrap">
<span class="currency">$</span>
<input id="amountInput" type="number" min="0" step="10" placeholder="0" required />
</div>
</div>
<button type="submit" class="btn-add">Add expense</button>
</form>
</section>
<section class="summary" aria-label="Budget summary">
<div class="tile">
<span class="tile-label">Estimated total</span>
<span class="tile-val" id="estTotal">$0</span>
</div>
<div class="tile">
<span class="tile-label">Spent total</span>
<span class="tile-val" id="spentTile">$0</span>
</div>
<div class="tile">
<span class="tile-label">Remaining</span>
<span class="tile-val" id="remainTile">$0</span>
</div>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A romantic, print-inspired budget tracker for couples planning their wedding. A header sets the total budget beside a live progress bar that fills toward the ceiling and blushes into a warm alert when spending overshoots. Beneath it, a table of category rows — venue, catering, florals, photography and more — lays each estimated figure against the actual amount recorded, with a per-row remaining balance and a small badge that flips to a gold warning when a category runs over.
The add-expense form lets you pick a category, name the line item and log an amount; the matching row’s actual total, remaining balance and the master progress bar all update instantly, with a toast confirming the entry. Summary tiles at the foot keep the estimated total, spent total and remaining balance in view at a glance.
Everything is vanilla HTML, CSS and JavaScript — no build step, no dependencies. State lives in a small in-memory model, currency is formatted for readability, and the layout collapses gracefully to a single airy column on narrow screens while keeping controls keyboard-friendly with visible focus rings.