Travel — Trip Budget Indicator
A trip budget planner that turns numbers into a clear money picture for a fictional Kyoto and Inland Sea journey. Set a total budget, then split it across flights, stay, food, activities and transport with paired sliders and currency-formatted inputs. A single stacked bar shows each category as a coloured segment, a live summary reports allocated, remaining, per-day and per-person figures, and the bar plus remaining value flip to a striped red over-budget state with a toast whenever spending tips past the limit.
MCP
Code
:root {
--bg: #fbf7f1;
--surface: #fffdf9;
--ink: #241f1a;
--muted: #6b6259;
--teal: #1f8a8a;
--coral: #e8623f;
--sand: #e7d8c3;
--line: rgba(36, 31, 26, 0.12);
--line-strong: rgba(36, 31, 26, 0.22);
--ok: #2f8f5b;
--warn: #c4452b;
--track: #efe6d8;
/* category accents */
--c-flights: #1f8a8a;
--c-stay: #e8623f;
--c-food: #d9a02b;
--c-activities: #7b5ea7;
--c-transport: #3f7bb6;
--radius: 16px;
--radius-sm: 10px;
--shadow: 0 1px 2px rgba(36, 31, 26, 0.05), 0 16px 40px -24px rgba(36, 31, 26, 0.35);
--serif: "Fraunces", Georgia, "Times New Roman", serif;
--sans: "Work Sans", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: var(--sans);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
img,
svg {
display: block;
}
button,
input {
font: inherit;
color: inherit;
}
:focus-visible {
outline: 3px solid color-mix(in srgb, var(--teal) 60%, white);
outline-offset: 2px;
border-radius: 6px;
}
.skip {
position: absolute;
left: 12px;
top: -48px;
background: var(--ink);
color: #fff;
padding: 0.6rem 0.9rem;
border-radius: 8px;
z-index: 20;
transition: top 0.15s ease;
}
.skip:focus {
top: 12px;
}
.page {
max-width: 880px;
margin: 0 auto;
padding: 0 18px 56px;
}
/* ---------- Hero ---------- */
.hero {
position: relative;
margin: 22px -18px 0;
border-radius: 0;
overflow: hidden;
isolation: isolate;
}
@media (min-width: 720px) {
.hero {
margin: 26px 0 0;
border-radius: var(--radius);
}
}
.hero__scene {
position: absolute;
inset: 0;
z-index: -1;
}
.hero__svg {
width: 100%;
height: 100%;
}
.hero__inner {
padding: 84px 22px 24px;
background: linear-gradient(
to top,
rgba(36, 31, 26, 0.55),
rgba(36, 31, 26, 0.12) 55%,
rgba(36, 31, 26, 0)
);
}
.hero__kicker {
margin: 0 0 0.4rem;
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
font-weight: 600;
color: #fdeede;
}
.hero__title {
margin: 0;
font-family: var(--serif);
font-weight: 600;
font-size: clamp(1.9rem, 6vw, 3rem);
line-height: 1.04;
color: #fffaf2;
letter-spacing: -0.01em;
}
.hero__sub {
margin: 0.5rem 0 0;
color: #fbeede;
font-weight: 500;
font-size: 0.95rem;
}
/* ---------- Cards ---------- */
.planner {
display: grid;
gap: 18px;
margin-top: 18px;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: clamp(18px, 4vw, 26px);
}
.card__title {
margin: 0;
font-family: var(--serif);
font-weight: 600;
font-size: 1.3rem;
letter-spacing: -0.01em;
}
.card__hint {
margin: 0.3rem 0 0;
color: var(--muted);
font-size: 0.9rem;
}
/* ---------- Budget top ---------- */
.budget__top {
display: flex;
flex-wrap: wrap;
gap: 18px;
align-items: flex-end;
justify-content: space-between;
}
.budget__field {
flex: 0 0 auto;
}
.budget__label {
display: block;
margin: 0 0 0.35rem;
font-size: 0.74rem;
letter-spacing: 0.06em;
text-transform: uppercase;
font-weight: 600;
color: var(--muted);
}
.budget__label--quiet {
margin: 0.35rem 0 0;
text-transform: none;
letter-spacing: 0;
font-weight: 500;
}
.money-input {
display: inline-flex;
align-items: center;
gap: 0.35rem;
border: 1.5px solid var(--line-strong);
background: var(--bg);
border-radius: var(--radius-sm);
padding: 0.5rem 0.8rem;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.money-input:focus-within {
border-color: var(--teal);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--teal) 22%, transparent);
}
.money-input__sym {
color: var(--muted);
font-weight: 600;
}
.money-input__field {
width: 9ch;
border: 0;
background: transparent;
font-family: var(--serif);
font-weight: 600;
font-size: 1.4rem;
letter-spacing: -0.01em;
}
.money-input__field:focus {
outline: none;
}
/* hide number spinners for a cleaner money UI */
.money-input__field::-webkit-outer-spin-button,
.money-input__field::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.money-input__field[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
/* ---------- Stacked bar ---------- */
.bar {
position: relative;
margin-top: 22px;
display: flex;
height: 30px;
width: 100%;
background: var(--track);
border-radius: 999px;
overflow: hidden;
border: 1px solid var(--line);
box-shadow: inset 0 1px 2px rgba(36, 31, 26, 0.08);
}
/* budget-limit marker, only present when over budget. Kept inside the bar so
the parent's overflow:hidden (for rounded ends) doesn't clip it. */
.bar__limit {
position: absolute;
top: 0;
bottom: 0;
width: 3px;
transform: translateX(-50%);
background: var(--surface);
box-shadow: 0 0 0 1px var(--warn);
z-index: 2;
}
.bar.is-over {
box-shadow: inset 0 1px 2px rgba(36, 31, 26, 0.08), 0 0 0 2px color-mix(in srgb, var(--warn) 55%, transparent);
}
.seg {
height: 100%;
min-width: 0;
transition: width 0.4s cubic-bezier(0.22, 0.61, 0.36, 1);
position: relative;
}
.seg + .seg {
box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.35);
}
.seg--over {
background: repeating-linear-gradient(
45deg,
var(--warn),
var(--warn) 7px,
color-mix(in srgb, var(--warn) 78%, #000) 7px,
color-mix(in srgb, var(--warn) 78%, #000) 14px
);
transition: width 0.4s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.bar__scale {
display: flex;
justify-content: space-between;
margin: 0.5rem 0 0;
font-size: 0.74rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
font-weight: 500;
}
/* ---------- Legend ---------- */
.legend {
display: flex;
flex-wrap: wrap;
gap: 0.4rem 1rem;
margin: 0.9rem 0 0;
}
.legend__item {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.82rem;
color: var(--muted);
font-weight: 500;
}
.legend__dot {
width: 11px;
height: 11px;
border-radius: 4px;
flex: 0 0 auto;
}
/* ---------- Summary ---------- */
.summary {
margin-top: 1.4rem;
padding-top: 1.2rem;
border-top: 1px solid var(--line);
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.9rem 1rem;
}
@media (min-width: 560px) {
.summary {
grid-template-columns: repeat(4, 1fr);
}
}
.summary__item {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.summary__label {
font-size: 0.72rem;
letter-spacing: 0.06em;
text-transform: uppercase;
font-weight: 600;
color: var(--muted);
}
.summary__value {
font-family: var(--serif);
font-weight: 600;
font-size: clamp(1.15rem, 4vw, 1.5rem);
letter-spacing: -0.01em;
font-variant-numeric: tabular-nums;
}
.summary__item--remaining .summary__value {
color: var(--ok);
}
.summary__item--remaining.is-over .summary__value {
color: var(--warn);
}
.summary__item--remaining.is-over .summary__label {
color: var(--warn);
}
/* ---------- Categories ---------- */
.categories__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 0.6rem;
}
.rows {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.5rem;
}
.row {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 0.5rem 1rem;
padding: 0.85rem 0.4rem;
border-bottom: 1px solid var(--line);
}
.row:last-child {
border-bottom: 0;
}
.row__head {
display: flex;
align-items: center;
gap: 0.65rem;
min-width: 0;
}
.row__icon {
width: 34px;
height: 34px;
flex: 0 0 auto;
border-radius: 10px;
display: grid;
place-items: center;
font-size: 1.05rem;
background: color-mix(in srgb, var(--accent) 16%, white);
}
.row__name {
display: flex;
flex-direction: column;
min-width: 0;
}
.row__title {
font-weight: 600;
font-size: 0.98rem;
white-space: nowrap;
}
.row__pct {
font-size: 0.76rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.row__slider {
grid-column: 1 / -1;
width: 100%;
margin: 0.1rem 0 0;
}
@media (min-width: 560px) {
.row {
grid-template-columns: minmax(160px, auto) 1fr auto;
}
.row__slider {
grid-column: auto;
margin: 0;
}
}
/* range styling */
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 22px;
background: transparent;
cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
height: 6px;
border-radius: 999px;
background: linear-gradient(
to right,
var(--accent) calc(var(--fill, 0) * 1%),
var(--track) calc(var(--fill, 0) * 1%)
);
}
input[type="range"]::-moz-range-track {
height: 6px;
border-radius: 999px;
background: var(--track);
}
input[type="range"]::-moz-range-progress {
height: 6px;
border-radius: 999px;
background: var(--accent);
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
margin-top: -8px;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--surface);
border: 2px solid var(--accent);
box-shadow: 0 1px 3px rgba(36, 31, 26, 0.25);
}
input[type="range"]::-moz-range-thumb {
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--surface);
border: 2px solid var(--accent);
box-shadow: 0 1px 3px rgba(36, 31, 26, 0.25);
}
input[type="range"]:focus-visible::-webkit-slider-thumb {
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 32%, transparent);
}
input[type="range"]:focus-visible::-moz-range-thumb {
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 32%, transparent);
}
.row__amount {
justify-self: end;
}
.row__amount .money-input {
padding: 0.3rem 0.55rem;
}
.row__amount .money-input__field {
font-size: 1rem;
width: 6.5ch;
}
/* ---------- Buttons ---------- */
.btn {
border: 1.5px solid transparent;
border-radius: 999px;
padding: 0.55rem 1rem;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.05s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn--ghost {
background: transparent;
border-color: var(--line-strong);
color: var(--ink);
}
.btn--ghost:hover {
border-color: var(--teal);
color: var(--teal);
background: color-mix(in srgb, var(--teal) 8%, transparent);
}
/* ---------- Footer ---------- */
.foot {
margin-top: 26px;
text-align: center;
color: var(--muted);
font-size: 0.82rem;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 22px;
transform: translate(-50%, 18px);
background: var(--ink);
color: #fff;
padding: 0.7rem 1.1rem;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 500;
box-shadow: 0 12px 30px -10px rgba(36, 31, 26, 0.6);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 30;
max-width: calc(100% - 32px);
text-align: center;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
.toast--warn {
background: var(--warn);
}
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;
}
}(function () {
"use strict";
/* ---------- Data ---------- */
var categories = [
{ id: "flights", label: "Flights", icon: "✈️", amount: 2100, accent: "var(--c-flights)" },
{ id: "stay", label: "Stay", icon: "🏨", amount: 1860, accent: "var(--c-stay)" },
{ id: "food", label: "Food", icon: "🍜", amount: 1080, accent: "var(--c-food)" },
{ id: "activities", label: "Activities", icon: "⛩️", amount: 760, accent: "var(--c-activities)" },
{ id: "transport", label: "Transport", icon: "🚅", amount: 400, accent: "var(--c-transport)" }
];
// immutable copy of the suggested defaults for "reset"
var defaults = categories.map(function (c) {
return c.amount;
});
var TRIP_DAYS = 12;
var TRAVELLERS = 2;
/* ---------- Helpers ---------- */
var fmt = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0
});
function money(n) {
return fmt.format(Math.round(n));
}
function clamp(n, min, max) {
return Math.min(max, Math.max(min, n));
}
/* ---------- Elements ---------- */
var totalInput = document.getElementById("total");
var bar = document.getElementById("bar");
var legend = document.getElementById("legend");
var rowsEl = document.getElementById("rows");
var scaleMid = document.getElementById("scale-mid");
var scaleEnd = document.getElementById("scale-end");
var sumAllocated = document.getElementById("sum-allocated");
var sumRemaining = document.getElementById("sum-remaining");
var sumPerday = document.getElementById("sum-perday");
var sumPerperson = document.getElementById("sum-perperson");
var remainingItem = document.getElementById("remaining-item");
var remainingLabel = document.getElementById("remaining-label");
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg, warn) {
toastEl.textContent = msg;
toastEl.classList.toggle("toast--warn", !!warn);
toastEl.classList.add("is-show");
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2600);
}
function getTotal() {
var v = parseFloat(totalInput.value);
return isNaN(v) || v < 0 ? 0 : v;
}
/* ---------- Build rows ---------- */
// Slider range adapts to budget so categories can span a useful range.
function sliderMax() {
return Math.max(getTotal(), 1000);
}
categories.forEach(function (cat) {
var li = document.createElement("li");
li.className = "row";
li.style.setProperty("--accent", cat.accent);
li.dataset.id = cat.id;
li.innerHTML =
'<div class="row__head">' +
'<span class="row__icon" aria-hidden="true">' + cat.icon + "</span>" +
'<span class="row__name">' +
'<span class="row__title">' + cat.label + "</span>" +
'<span class="row__pct" data-pct>0% of budget</span>' +
"</span>" +
"</div>" +
'<input class="row__slider" type="range" min="0" step="20" ' +
'aria-label="' + cat.label + ' amount" />' +
'<span class="row__amount">' +
'<span class="money-input">' +
'<span class="money-input__sym" aria-hidden="true">$</span>' +
'<input class="money-input__field" type="number" min="0" step="20" inputmode="numeric" ' +
'aria-label="' + cat.label + ' amount in dollars" />' +
"</span>" +
"</span>";
rowsEl.appendChild(li);
cat._row = li;
cat._slider = li.querySelector(".row__slider");
cat._number = li.querySelector(".money-input__field");
cat._pct = li.querySelector("[data-pct]");
// slider drag → update amount live
cat._slider.addEventListener("input", function () {
cat.amount = clamp(parseFloat(cat._slider.value) || 0, 0, sliderMax());
render();
});
// number field → update amount (allow exceeding slider max)
cat._number.addEventListener("input", function () {
var v = parseFloat(cat._number.value);
cat.amount = isNaN(v) || v < 0 ? 0 : v;
render();
});
// normalise on blur (no negatives / NaN left behind)
cat._number.addEventListener("blur", function () {
cat._number.value = Math.round(cat.amount);
});
});
// build legend chips once
categories.forEach(function (cat) {
var item = document.createElement("span");
item.className = "legend__item";
item.innerHTML =
'<span class="legend__dot" style="background:' + cat.accent + '"></span>' +
cat.label +
' <strong data-leg="' + cat.id + '" style="margin-left:.15rem">$0</strong>';
legend.appendChild(item);
});
/* ---------- Render ---------- */
function render() {
var total = getTotal();
var allocated = categories.reduce(function (s, c) {
return s + c.amount;
}, 0);
var remaining = total - allocated;
var over = remaining < 0;
// denominator for segment widths: stretch to allocated when over budget so
// every category stays visible; the budget line then sits before the end.
var scaleTotal = Math.max(total, allocated, 1);
// rebuild stacked bar
bar.innerHTML = "";
categories.forEach(function (cat) {
var pctOfScale = (cat.amount / scaleTotal) * 100;
var seg = document.createElement("span");
seg.className = "seg";
seg.style.width = pctOfScale + "%";
seg.style.background = cat.accent;
seg.title = cat.label + " · " + money(cat.amount);
bar.appendChild(seg);
// sliders/pcts
var max = sliderMax();
cat._slider.max = max;
cat._slider.value = Math.min(cat.amount, max);
cat._slider.style.setProperty("--fill", ((Math.min(cat.amount, max) / max) * 100).toFixed(2));
if (document.activeElement !== cat._number) {
cat._number.value = Math.round(cat.amount);
}
var pctOfBudget = total > 0 ? (cat.amount / total) * 100 : 0;
cat._pct.textContent = Math.round(pctOfBudget) + "% of budget";
var legVal = legend.querySelector('[data-leg="' + cat.id + '"]');
if (legVal) legVal.textContent = money(cat.amount);
});
// when over budget, the five segments fill the bar (scaled to allocated);
// drop a "budget line" marker showing where the limit falls, plus stripe the
// portion of each segment beyond it via the bar's over state.
if (over) {
var marker = document.createElement("span");
marker.className = "bar__limit";
marker.style.left = (total / scaleTotal) * 100 + "%";
marker.title = "Budget limit · " + money(total);
bar.appendChild(marker);
}
bar.classList.toggle("is-over", over);
bar.setAttribute(
"aria-label",
"Budget bar. Allocated " +
money(allocated) +
" of " +
money(total) +
". " +
(over ? "Over budget by " + money(Math.abs(remaining)) : money(remaining) + " remaining")
);
// scale labels
scaleMid.textContent = money(scaleTotal / 2);
scaleEnd.textContent = money(scaleTotal);
// summary
sumAllocated.textContent = money(allocated);
sumPerday.textContent = money(allocated / TRIP_DAYS);
sumPerperson.textContent = money(allocated / TRIP_DAYS / TRAVELLERS);
if (over) {
remainingLabel.textContent = "Over budget";
sumRemaining.textContent = "−" + money(Math.abs(remaining));
remainingItem.classList.add("is-over");
} else {
remainingLabel.textContent = "Remaining";
sumRemaining.textContent = money(remaining);
remainingItem.classList.remove("is-over");
}
}
/* ---------- Total budget input ---------- */
var wasOver = false;
totalInput.addEventListener("input", function () {
render();
var total = getTotal();
var allocated = categories.reduce(function (s, c) {
return s + c.amount;
}, 0);
var nowOver = allocated > total;
if (nowOver && !wasOver) {
toast("Over budget by " + money(allocated - total), true);
}
wasOver = nowOver;
});
totalInput.addEventListener("blur", function () {
if (getTotal() === 0) toast("Set a budget to see remaining per day");
});
/* ---------- Reset ---------- */
document.getElementById("reset").addEventListener("click", function () {
categories.forEach(function (cat, i) {
cat.amount = defaults[i];
});
totalInput.value = 6400;
wasOver = false;
render();
toast("Reset to suggested split");
});
/* ---------- Warn-once when a slider drag tips over budget ---------- */
categories.forEach(function (cat) {
cat._slider.addEventListener("change", function () {
var total = getTotal();
var allocated = categories.reduce(function (s, c) {
return s + c.amount;
}, 0);
if (allocated > total && !wasOver) {
toast("That tips you " + money(allocated - total) + " over budget", true);
}
wasOver = allocated > total;
});
});
/* ---------- Init ---------- */
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Trip Budget Indicator — Kyoto & the Inland Sea</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=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Work+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip" href="#planner">Skip to budget planner</a>
<div class="page">
<header class="hero" role="banner">
<div class="hero__scene" aria-hidden="true">
<svg viewBox="0 0 1200 320" preserveAspectRatio="xMidYMax slice" class="hero__svg">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f7d7b8" />
<stop offset="0.55" stop-color="#f2b894" />
<stop offset="1" stop-color="#e89a7c" />
</linearGradient>
<linearGradient id="far" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#9fb3a8" />
<stop offset="1" stop-color="#84a097" />
</linearGradient>
<linearGradient id="mid" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#5f877e" />
<stop offset="1" stop-color="#3f6a62" />
</linearGradient>
<linearGradient id="water" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2f7d7d" />
<stop offset="1" stop-color="#1f5f63" />
</linearGradient>
</defs>
<rect width="1200" height="320" fill="url(#sky)" />
<circle cx="930" cy="92" r="46" fill="#fff1de" opacity="0.85" />
<path d="M0 196 L150 150 L300 188 L470 132 L640 184 L820 140 L1000 186 L1200 150 L1200 320 L0 320 Z" fill="url(#far)" opacity="0.9" />
<path d="M0 232 L180 196 L360 236 L540 188 L720 232 L900 196 L1080 234 L1200 206 L1200 320 L0 320 Z" fill="url(#mid)" />
<rect y="262" width="1200" height="58" fill="url(#water)" />
<g fill="#fbf4ec" opacity="0.5">
<path d="M120 282 q40 -8 80 0 t80 0" stroke="#fbf4ec" stroke-width="2" fill="none" />
<path d="M520 296 q40 -8 80 0 t80 0" stroke="#fbf4ec" stroke-width="2" fill="none" />
<path d="M900 286 q40 -8 80 0 t80 0" stroke="#fbf4ec" stroke-width="2" fill="none" />
</g>
</svg>
</div>
<div class="hero__inner">
<p class="hero__kicker">Field Notes · Trip Planner</p>
<h1 class="hero__title">Kyoto & the Inland Sea</h1>
<p class="hero__sub">12 days · 2 travellers · spring shoulder season</p>
</div>
</header>
<main id="planner" class="planner" role="main">
<section class="card budget" aria-labelledby="budget-h">
<div class="budget__top">
<div>
<h2 id="budget-h" class="card__title">Total budget</h2>
<p class="card__hint">Set what you have to spend, then split it across categories.</p>
</div>
<div class="budget__field">
<label class="budget__label" for="total">Budget (USD)</label>
<div class="money-input">
<span class="money-input__sym" aria-hidden="true">$</span>
<input
id="total"
class="money-input__field"
type="number"
inputmode="numeric"
min="0"
step="50"
value="6400"
aria-describedby="total-help"
/>
</div>
<p id="total-help" class="budget__label budget__label--quiet">over 12 days · 2 people</p>
</div>
</div>
<div class="bar" role="img" aria-labelledby="budget-h" id="bar">
<!-- segments injected by JS -->
</div>
<p class="bar__scale" aria-hidden="true">
<span>$0</span>
<span id="scale-mid">$3,200</span>
<span id="scale-end">$6,400</span>
</p>
<div class="legend" id="legend" aria-hidden="true">
<!-- legend chips injected by JS -->
</div>
<div class="summary" aria-live="polite">
<div class="summary__item">
<span class="summary__label">Allocated</span>
<strong class="summary__value" id="sum-allocated">$0</strong>
</div>
<div class="summary__item summary__item--remaining" id="remaining-item">
<span class="summary__label" id="remaining-label">Remaining</span>
<strong class="summary__value" id="sum-remaining">$0</strong>
</div>
<div class="summary__item">
<span class="summary__label">Per day</span>
<strong class="summary__value" id="sum-perday">$0</strong>
</div>
<div class="summary__item">
<span class="summary__label">Per person / day</span>
<strong class="summary__value" id="sum-perperson">$0</strong>
</div>
</div>
</section>
<section class="card categories" aria-labelledby="cats-h">
<div class="categories__head">
<h2 id="cats-h" class="card__title">Where it goes</h2>
<button id="reset" class="btn btn--ghost" type="button">Reset to suggested</button>
</div>
<ul class="rows" id="rows">
<!-- category rows injected by JS -->
</ul>
</section>
</main>
<footer class="foot" role="contentinfo">
<p>Illustrative travel UI — fictional destinations, prices, and figures.</p>
</footer>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Trip Budget Indicator
A compact budget planner for a fictional Kyoto & the Inland Sea trip. A serif editorial hero with a layered CSS/SVG coastline sets the mood, then a money-first card lets you type a total budget and watch it split across five categories — flights, stay, food, activities and transport. Each row pairs a coloured slider with a currency input so you can drag or type, and every category shows its share as a percentage of the budget.
The heart of the component is one stacked budget bar: each category is a coloured segment whose
width tracks its amount, with a matching legend and a scale beneath. A live summary reports the
allocated total, what is remaining, and the per-day and per-person-per-day figures, all formatted as
USD with Intl.NumberFormat. The moment your allocations exceed the budget, the bar gains a striped
red overspend marker, the remaining value flips to a red “Over budget” state, and a toast tells you
exactly how far over you are.
Every interaction works in vanilla JS with no libraries. Sliders and number fields stay in sync, “Reset to suggested” restores a sensible default split, the slider range adapts to your budget, and controls are keyboard-usable with visible focus rings. Contrast meets WCAG AA and the layout collapses gracefully to a single column down to ~360px.
Illustrative travel UI only — fictional destinations, prices, and maps.