More Dashboards Snippets
Budget Tracker Card — Spending by Category HTML/CSS/JS
Budget Tracker Card · Dashboards · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Budget Tracker Card — Overall Progress, Per-Category Caps & Over-Budget Warnings
A budget tracker only earns trust if it's honest about overspending — a green bar that quietly turns red is far more useful than a generic progress indicator. This snippet builds a monthly budget card with an overall spend bar that shifts from indigo to amber to red as it fills, plus a per-category breakdown that flags any category individually over its own cap, all driven by one small data array.
One data array drives everything
CATEGORIES is a plain array of { name, color, spent, cap } objects, and BUDGET_CAP is the single overall monthly limit. render() sums every category's spent for the total, computes the overall percentage against BUDGET_CAP, and rebuilds the category list — so updating a single number anywhere in the data instantly recomputes the whole card with one function call, no manual DOM bookkeeping per field.
Three-state overall bar
The top progress bar's fill width is the spend percentage capped at 100% (so an over-budget month doesn't overflow the track), and its color escalates through three CSS classes: the default indigo-to-purple gradient under 80% used, an amber-to-orange .warn gradient from 80–100%, and a red .over gradient once total spend exceeds the cap. The label beneath switches wording too — "$X left this month" while under budget, "$X over budget" once it isn't — so the message matches the math instead of always reading as a vague percentage.
Per-category caps, independent of the overall total
Each category has its own cap, and a category can go over its own cap (shown in red, with the row's amount text turning red via an .over class) even while the *overall* budget is still healthy — exactly how real budgeting works: groceries can run over while the month as a whole is fine because other categories came in under. This nuance is what makes a per-category breakdown more useful than a single combined bar.
Smooth, export-safe fills
Every bar fill transitions only its width with a cubic-bezier ease — width transitions on a fixed-height track are stable in every export target because the track's own dimensions never change, only the inner fill's width, so there's no layout-shift risk the way animating a container's height would have.
Connecting real transaction data
The numbers here are static placeholders meant to demonstrate the visual states; a production budget tracker sums real transactions grouped by category, typically from a bank-linking API (Plaid and similar providers) or a manually logged expense table. Because every visual — bar width, color, and label wording — is recomputed from CATEGORIES and BUDGET_CAP inside a single render() call, swapping the data source is a one-line change: replace the static spent values with a sum of that period's transactions per category, call render() again, and the card's entire visual state (including which categories flip to the over-budget red) updates correctly with no other code to touch.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA budget card renders with an overall progress bar and five category rows (Groceries, Dining, Transport, Entertainment, Shopping).
- 2Read the overall barIts color and the label beneath show whether you're under budget, in the 80%+ warning zone, or over for the month.
- 3Check each categoryCategories over their own cap show in red even if the overall budget is still healthy — read each row independently.
- 4Edit the dataChange any spent or cap value in the CATEGORIES array, or BUDGET_CAP, and call render() to see every bar and label recompute.
- 5Add or remove categoriesPush or remove an object in CATEGORIES — the category list and overall total both adjust automatically.
- 6Connect real transaction dataReplace the static spent values with a sum of your transaction API's amounts grouped by category, then call render() after each fetch.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Fetch your transactions, group and sum them by category into the same { name, color, spent, cap } shape used by CATEGORIES, assign the result to CATEGORIES, and call render() — every bar, label, and color recomputes from the new totals.
Edit the thresholds in render(): the ".warn" class is added when pct > 80, and ".over" when totalSpent > BUDGET_CAP — change 80 to whatever warning threshold fits your use case.
Add an input bound to BUDGET_CAP (or each category's cap) and call render() on change — the existing percentage math and color thresholds will reflect the new limit immediately.
The card has no built-in time period logic — it just compares spent against cap. Swap BUDGET_CAP and the category caps for your daily/weekly limits, and update the header text ("June 2026") to match your period.
In React, keep categories and budgetCap in state and compute percentages with useMemo, rendering rows with .map(); in Vue, use a computed categories array with v-for; in Angular, use *ngFor with a getter for percentages. The color-threshold logic ports directly into each framework's conditional class binding.
Budget Tracker Card — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Budget Tracker Card</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.bt-card{background:#fff;border-radius:18px;padding:22px;width:100%;max-width:420px;box-shadow:0 18px 44px rgba(15,23,42,.1)}
.bt-head{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:14px}
.bt-head h3{font-size:16px;font-weight:800;color:#0f172a}
.bt-head p{font-size:12px;color:#94a3b8;margin-top:2px}
.bt-total{text-align:right}
.bt-total span{font-size:18px;font-weight:800;color:#0f172a;font-variant-numeric:tabular-nums}
.bt-of{font-size:12px;font-weight:600;color:#94a3b8}
.bt-overall{margin-bottom:18px}
.bt-overall-track{height:9px;background:#f1f5f9;border-radius:999px;overflow:hidden}
.bt-overall-fill{height:100%;width:0;border-radius:999px;background:linear-gradient(90deg,#6366f1,#8b5cf6);transition:width .6s cubic-bezier(.4,0,.2,1)}
.bt-overall-fill.warn{background:linear-gradient(90deg,#f59e0b,#f97316)}
.bt-overall-fill.over{background:linear-gradient(90deg,#ef4444,#dc2626)}
.bt-overall-label{display:block;margin-top:6px;font-size:11.5px;font-weight:600;color:#64748b}
.bt-cats{display:flex;flex-direction:column;gap:13px}
.bt-cat-row{display:flex;flex-direction:column;gap:5px}
.bt-cat-top{display:flex;align-items:center;justify-content:space-between;font-size:13px}
.bt-cat-name{display:flex;align-items:center;gap:7px;font-weight:700;color:#1e293b}
.bt-cat-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.bt-cat-amt{font-weight:700;color:#475569;font-variant-numeric:tabular-nums;font-size:12.5px}
.bt-cat-amt .bt-cap{color:#cbd5e1;font-weight:600}
.bt-cat-track{height:6px;background:#f1f5f9;border-radius:999px;overflow:hidden}
.bt-cat-fill{height:100%;border-radius:999px;transition:width .6s cubic-bezier(.4,0,.2,1)}
.bt-cat-row.over .bt-cat-amt{color:#dc2626}
</style>
</head>
<body>
<div class="bt-card">
<div class="bt-head">
<div>
<h3>Monthly budget</h3>
<p>June 2026</p>
</div>
<div class="bt-total">
<span id="btSpent">$0</span> <span class="bt-of">of $2,400</span>
</div>
</div>
<div class="bt-overall">
<div class="bt-overall-track"><div class="bt-overall-fill" id="btOverallFill"></div></div>
<span class="bt-overall-label" id="btOverallLabel"></span>
</div>
<div class="bt-cats" id="btCats"></div>
</div>
<script>
var CATEGORIES = [
{ name: 'Groceries', color: '#22c55e', spent: 380, cap: 500 },
{ name: 'Dining out', color: '#f59e0b', spent: 265, cap: 250 },
{ name: 'Transport', color: '#6366f1', spent: 140, cap: 300 },
{ name: 'Entertainment', color: '#ec4899', spent: 95, cap: 150 },
{ name: 'Shopping', color: '#0ea5e9', spent: 410, cap: 400 },
];
var BUDGET_CAP = 2400;
function render() {
var totalSpent = CATEGORIES.reduce(function (s, c) { return s + c.spent; }, 0);
document.getElementById('btSpent').textContent = '$' + totalSpent.toLocaleString();
var pct = Math.min(100, (totalSpent / BUDGET_CAP) * 100);
var overallFill = document.getElementById('btOverallFill');
overallFill.style.width = pct + '%';
overallFill.className = 'bt-overall-fill' + (totalSpent > BUDGET_CAP ? ' over' : pct > 80 ? ' warn' : '');
var remaining = BUDGET_CAP - totalSpent;
document.getElementById('btOverallLabel').textContent = remaining >= 0
? '$' + remaining.toLocaleString() + ' left this month (' + Math.round(pct) + '% used)'
: '$' + Math.abs(remaining).toLocaleString() + ' over budget';
var cats = document.getElementById('btCats');
cats.innerHTML = CATEGORIES.map(function (c) {
var catPct = Math.min(100, (c.spent / c.cap) * 100);
var over = c.spent > c.cap;
return '<div class="bt-cat-row' + (over ? ' over' : '') + '">' +
'<div class="bt-cat-top">' +
'<span class="bt-cat-name"><i class="bt-cat-dot" style="background:' + c.color + '"></i>' + c.name + '</span>' +
'<span class="bt-cat-amt">$' + c.spent + ' <span class="bt-cap">/ $' + c.cap + '</span></span>' +
'</div>' +
'<div class="bt-cat-track"><div class="bt-cat-fill" style="width:' + catPct + '%;background:' + (over ? '#ef4444' : c.color) + '"></div></div>' +
'</div>';
}).join('');
}
render();
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Budget Tracker Card</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}
.bt-head h3 {
font-size:16px;font-weight:800;color:#0f172a
}
.bt-head p {
font-size:12px;color:#94a3b8;margin-top:2px
}
.bt-total span {
font-size:18px;font-weight:800;color:#0f172a;font-variant-numeric:tabular-nums
}
.bt-overall-fill {
height:100%;width:0;border-radius:999px;background:linear-gradient(90deg,#6366f1,#8b5cf6);transition:width .6s cubic-bezier(.4,0,.2,1)
}
.bt-overall-fill.warn {
background:linear-gradient(90deg,#f59e0b,#f97316)
}
.bt-overall-fill.over {
background:linear-gradient(90deg,#ef4444,#dc2626)
}
.bt-cat-row {
display:flex;flex-direction:column;gap:5px
}
.bt-cat-top {
display:flex;align-items:center;justify-content:space-between;font-size:13px
}
.bt-cat-name {
display:flex;align-items:center;gap:7px;font-weight:700;color:#1e293b
}
.bt-cat-dot {
width:8px;height:8px;border-radius:50%;flex-shrink:0
}
.bt-cat-amt {
font-weight:700;color:#475569;font-variant-numeric:tabular-nums;font-size:12.5px
}
.bt-cat-amt .bt-cap {
color:#cbd5e1;font-weight:600
}
.bt-cat-track {
height:6px;background:#f1f5f9;border-radius:999px;overflow:hidden
}
.bt-cat-fill {
height:100%;border-radius:999px;transition:width .6s cubic-bezier(.4,0,.2,1)
}
.bt-cat-row.over .bt-cat-amt {
color:#dc2626
}
</style>
</head>
<body>
<div class="bg-[#fff] rounded-[18px] p-[22px] w-full max-w-[420px] shadow-[0_18px_44px_rgba(15,23,42,.1)]">
<div class="bt-head flex items-start justify-between mb-3.5">
<div>
<h3>Monthly budget</h3>
<p>June 2026</p>
</div>
<div class="bt-total text-right">
<span id="btSpent">$0</span> <span class="text-xs font-semibold text-[#94a3b8]">of $2,400</span>
</div>
</div>
<div class="mb-[18px]">
<div class="h-[9px] bg-[#f1f5f9] rounded-[999px] overflow-hidden"><div class="bt-overall-fill" id="btOverallFill"></div></div>
<span class="block mt-1.5 text-[11.5px] font-semibold text-[#64748b]" id="btOverallLabel"></span>
</div>
<div class="flex flex-col gap-[13px]" id="btCats"></div>
</div>
<script>
var CATEGORIES = [
{ name: 'Groceries', color: '#22c55e', spent: 380, cap: 500 },
{ name: 'Dining out', color: '#f59e0b', spent: 265, cap: 250 },
{ name: 'Transport', color: '#6366f1', spent: 140, cap: 300 },
{ name: 'Entertainment', color: '#ec4899', spent: 95, cap: 150 },
{ name: 'Shopping', color: '#0ea5e9', spent: 410, cap: 400 },
];
var BUDGET_CAP = 2400;
function render() {
var totalSpent = CATEGORIES.reduce(function (s, c) { return s + c.spent; }, 0);
document.getElementById('btSpent').textContent = '$' + totalSpent.toLocaleString();
var pct = Math.min(100, (totalSpent / BUDGET_CAP) * 100);
var overallFill = document.getElementById('btOverallFill');
overallFill.style.width = pct + '%';
overallFill.className = 'bt-overall-fill' + (totalSpent > BUDGET_CAP ? ' over' : pct > 80 ? ' warn' : '');
var remaining = BUDGET_CAP - totalSpent;
document.getElementById('btOverallLabel').textContent = remaining >= 0
? '$' + remaining.toLocaleString() + ' left this month (' + Math.round(pct) + '% used)'
: '$' + Math.abs(remaining).toLocaleString() + ' over budget';
var cats = document.getElementById('btCats');
cats.innerHTML = CATEGORIES.map(function (c) {
var catPct = Math.min(100, (c.spent / c.cap) * 100);
var over = c.spent > c.cap;
return '<div class="bt-cat-row' + (over ? ' over' : '') + '">' +
'<div class="bt-cat-top">' +
'<span class="bt-cat-name"><i class="bt-cat-dot" style="background:' + c.color + '"></i>' + c.name + '</span>' +
'<span class="bt-cat-amt">$' + c.spent + ' <span class="bt-cap">/ $' + c.cap + '</span></span>' +
'</div>' +
'<div class="bt-cat-track"><div class="bt-cat-fill" style="width:' + catPct + '%;background:' + (over ? '#ef4444' : c.color) + '"></div></div>' +
'</div>';
}).join('');
}
render();
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to BudgetTrackerCard.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.bt-card{background:#fff;border-radius:18px;padding:22px;width:100%;max-width:420px;box-shadow:0 18px 44px rgba(15,23,42,.1)}
.bt-head{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:14px}
.bt-head h3{font-size:16px;font-weight:800;color:#0f172a}
.bt-head p{font-size:12px;color:#94a3b8;margin-top:2px}
.bt-total{text-align:right}
.bt-total span{font-size:18px;font-weight:800;color:#0f172a;font-variant-numeric:tabular-nums}
.bt-of{font-size:12px;font-weight:600;color:#94a3b8}
.bt-overall{margin-bottom:18px}
.bt-overall-track{height:9px;background:#f1f5f9;border-radius:999px;overflow:hidden}
.bt-overall-fill{height:100%;width:0;border-radius:999px;background:linear-gradient(90deg,#6366f1,#8b5cf6);transition:width .6s cubic-bezier(.4,0,.2,1)}
.bt-overall-fill.warn{background:linear-gradient(90deg,#f59e0b,#f97316)}
.bt-overall-fill.over{background:linear-gradient(90deg,#ef4444,#dc2626)}
.bt-overall-label{display:block;margin-top:6px;font-size:11.5px;font-weight:600;color:#64748b}
.bt-cats{display:flex;flex-direction:column;gap:13px}
.bt-cat-row{display:flex;flex-direction:column;gap:5px}
.bt-cat-top{display:flex;align-items:center;justify-content:space-between;font-size:13px}
.bt-cat-name{display:flex;align-items:center;gap:7px;font-weight:700;color:#1e293b}
.bt-cat-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.bt-cat-amt{font-weight:700;color:#475569;font-variant-numeric:tabular-nums;font-size:12.5px}
.bt-cat-amt .bt-cap{color:#cbd5e1;font-weight:600}
.bt-cat-track{height:6px;background:#f1f5f9;border-radius:999px;overflow:hidden}
.bt-cat-fill{height:100%;border-radius:999px;transition:width .6s cubic-bezier(.4,0,.2,1)}
.bt-cat-row.over .bt-cat-amt{color:#dc2626}
`;
export default function BudgetTrackerCard() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var CATEGORIES = [
{ name: 'Groceries', color: '#22c55e', spent: 380, cap: 500 },
{ name: 'Dining out', color: '#f59e0b', spent: 265, cap: 250 },
{ name: 'Transport', color: '#6366f1', spent: 140, cap: 300 },
{ name: 'Entertainment', color: '#ec4899', spent: 95, cap: 150 },
{ name: 'Shopping', color: '#0ea5e9', spent: 410, cap: 400 },
];
var BUDGET_CAP = 2400;
function render() {
var totalSpent = CATEGORIES.reduce(function (s, c) { return s + c.spent; }, 0);
document.getElementById('btSpent').textContent = '$' + totalSpent.toLocaleString();
var pct = Math.min(100, (totalSpent / BUDGET_CAP) * 100);
var overallFill = document.getElementById('btOverallFill');
overallFill.style.width = pct + '%';
overallFill.className = 'bt-overall-fill' + (totalSpent > BUDGET_CAP ? ' over' : pct > 80 ? ' warn' : '');
var remaining = BUDGET_CAP - totalSpent;
document.getElementById('btOverallLabel').textContent = remaining >= 0
? '$' + remaining.toLocaleString() + ' left this month (' + Math.round(pct) + '% used)'
: '$' + Math.abs(remaining).toLocaleString() + ' over budget';
var cats = document.getElementById('btCats');
cats.innerHTML = CATEGORIES.map(function (c) {
var catPct = Math.min(100, (c.spent / c.cap) * 100);
var over = c.spent > c.cap;
return '<div class="bt-cat-row' + (over ? ' over' : '') + '">' +
'<div class="bt-cat-top">' +
'<span class="bt-cat-name"><i class="bt-cat-dot" style="background:' + c.color + '"></i>' + c.name + '</span>' +
'<span class="bt-cat-amt">$' + c.spent + ' <span class="bt-cap">/ $' + c.cap + '</span></span>' +
'</div>' +
'<div class="bt-cat-track"><div class="bt-cat-fill" style="width:' + catPct + '%;background:' + (over ? '#ef4444' : c.color) + '"></div></div>' +
'</div>';
}).join('');
}
render();
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{css}</style>
<div className="bt-card">
<div className="bt-head">
<div>
<h3>Monthly budget</h3>
<p>June 2026</p>
</div>
<div className="bt-total">
<span id="btSpent">$0</span> <span className="bt-of">of $2,400</span>
</div>
</div>
<div className="bt-overall">
<div className="bt-overall-track"><div className="bt-overall-fill" id="btOverallFill"></div></div>
<span className="bt-overall-label" id="btOverallLabel"></span>
</div>
<div className="bt-cats" id="btCats"></div>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function BudgetTrackerCard() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var CATEGORIES = [
{ name: 'Groceries', color: '#22c55e', spent: 380, cap: 500 },
{ name: 'Dining out', color: '#f59e0b', spent: 265, cap: 250 },
{ name: 'Transport', color: '#6366f1', spent: 140, cap: 300 },
{ name: 'Entertainment', color: '#ec4899', spent: 95, cap: 150 },
{ name: 'Shopping', color: '#0ea5e9', spent: 410, cap: 400 },
];
var BUDGET_CAP = 2400;
function render() {
var totalSpent = CATEGORIES.reduce(function (s, c) { return s + c.spent; }, 0);
document.getElementById('btSpent').textContent = '$' + totalSpent.toLocaleString();
var pct = Math.min(100, (totalSpent / BUDGET_CAP) * 100);
var overallFill = document.getElementById('btOverallFill');
overallFill.style.width = pct + '%';
overallFill.className = 'bt-overall-fill' + (totalSpent > BUDGET_CAP ? ' over' : pct > 80 ? ' warn' : '');
var remaining = BUDGET_CAP - totalSpent;
document.getElementById('btOverallLabel').textContent = remaining >= 0
? '$' + remaining.toLocaleString() + ' left this month (' + Math.round(pct) + '% used)'
: '$' + Math.abs(remaining).toLocaleString() + ' over budget';
var cats = document.getElementById('btCats');
cats.innerHTML = CATEGORIES.map(function (c) {
var catPct = Math.min(100, (c.spent / c.cap) * 100);
var over = c.spent > c.cap;
return '<div class="bt-cat-row' + (over ? ' over' : '') + '">' +
'<div class="bt-cat-top">' +
'<span class="bt-cat-name"><i class="bt-cat-dot" style="background:' + c.color + '"></i>' + c.name + '</span>' +
'<span class="bt-cat-amt">$' + c.spent + ' <span class="bt-cap">/ $' + c.cap + '</span></span>' +
'</div>' +
'<div class="bt-cat-track"><div class="bt-cat-fill" style="width:' + catPct + '%;background:' + (over ? '#ef4444' : c.color) + '"></div></div>' +
'</div>';
}).join('');
}
render();
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{`
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}
.bt-head h3 {
font-size:16px;font-weight:800;color:#0f172a
}
.bt-head p {
font-size:12px;color:#94a3b8;margin-top:2px
}
.bt-total span {
font-size:18px;font-weight:800;color:#0f172a;font-variant-numeric:tabular-nums
}
.bt-overall-fill {
height:100%;width:0;border-radius:999px;background:linear-gradient(90deg,#6366f1,#8b5cf6);transition:width .6s cubic-bezier(.4,0,.2,1)
}
.bt-overall-fill.warn {
background:linear-gradient(90deg,#f59e0b,#f97316)
}
.bt-overall-fill.over {
background:linear-gradient(90deg,#ef4444,#dc2626)
}
.bt-cat-row {
display:flex;flex-direction:column;gap:5px
}
.bt-cat-top {
display:flex;align-items:center;justify-content:space-between;font-size:13px
}
.bt-cat-name {
display:flex;align-items:center;gap:7px;font-weight:700;color:#1e293b
}
.bt-cat-dot {
width:8px;height:8px;border-radius:50%;flex-shrink:0
}
.bt-cat-amt {
font-weight:700;color:#475569;font-variant-numeric:tabular-nums;font-size:12.5px
}
.bt-cat-amt .bt-cap {
color:#cbd5e1;font-weight:600
}
.bt-cat-track {
height:6px;background:#f1f5f9;border-radius:999px;overflow:hidden
}
.bt-cat-fill {
height:100%;border-radius:999px;transition:width .6s cubic-bezier(.4,0,.2,1)
}
.bt-cat-row.over .bt-cat-amt {
color:#dc2626
}
`}</style>
<div className="bg-[#fff] rounded-[18px] p-[22px] w-full max-w-[420px] shadow-[0_18px_44px_rgba(15,23,42,.1)]">
<div className="bt-head flex items-start justify-between mb-3.5">
<div>
<h3>Monthly budget</h3>
<p>June 2026</p>
</div>
<div className="bt-total text-right">
<span id="btSpent">$0</span> <span className="text-xs font-semibold text-[#94a3b8]">of $2,400</span>
</div>
</div>
<div className="mb-[18px]">
<div className="h-[9px] bg-[#f1f5f9] rounded-[999px] overflow-hidden"><div className="bt-overall-fill" id="btOverallFill"></div></div>
<span className="block mt-1.5 text-[11.5px] font-semibold text-[#64748b]" id="btOverallLabel"></span>
</div>
<div className="flex flex-col gap-[13px]" id="btCats"></div>
</div>
</>
);
}<template>
<div class="bt-card">
<div class="bt-head">
<div>
<h3>Monthly budget</h3>
<p>June 2026</p>
</div>
<div class="bt-total">
<span id="btSpent">$0</span> <span class="bt-of">of $2,400</span>
</div>
</div>
<div class="bt-overall">
<div class="bt-overall-track"><div class="bt-overall-fill" id="btOverallFill"></div></div>
<span class="bt-overall-label" id="btOverallLabel"></span>
</div>
<div class="bt-cats" id="btCats"></div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
var CATEGORIES = [
{ name: 'Groceries', color: '#22c55e', spent: 380, cap: 500 },
{ name: 'Dining out', color: '#f59e0b', spent: 265, cap: 250 },
{ name: 'Transport', color: '#6366f1', spent: 140, cap: 300 },
{ name: 'Entertainment', color: '#ec4899', spent: 95, cap: 150 },
{ name: 'Shopping', color: '#0ea5e9', spent: 410, cap: 400 },
];
var BUDGET_CAP = 2400;
function render() {
var totalSpent = CATEGORIES.reduce(function (s, c) { return s + c.spent; }, 0);
document.getElementById('btSpent').textContent = '$' + totalSpent.toLocaleString();
var pct = Math.min(100, (totalSpent / BUDGET_CAP) * 100);
var overallFill = document.getElementById('btOverallFill');
overallFill.style.width = pct + '%';
overallFill.className = 'bt-overall-fill' + (totalSpent > BUDGET_CAP ? ' over' : pct > 80 ? ' warn' : '');
var remaining = BUDGET_CAP - totalSpent;
document.getElementById('btOverallLabel').textContent = remaining >= 0
? '$' + remaining.toLocaleString() + ' left this month (' + Math.round(pct) + '% used)'
: '$' + Math.abs(remaining).toLocaleString() + ' over budget';
var cats = document.getElementById('btCats');
cats.innerHTML = CATEGORIES.map(function (c) {
var catPct = Math.min(100, (c.spent / c.cap) * 100);
var over = c.spent > c.cap;
return '<div class="bt-cat-row' + (over ? ' over' : '') + '">' +
'<div class="bt-cat-top">' +
'<span class="bt-cat-name"><i class="bt-cat-dot" style="background:' + c.color + '"></i>' + c.name + '</span>' +
'<span class="bt-cat-amt">$' + c.spent + ' <span class="bt-cap">/ $' + c.cap + '</span></span>' +
'</div>' +
'<div class="bt-cat-track"><div class="bt-cat-fill" style="width:' + catPct + '%;background:' + (over ? '#ef4444' : c.color) + '"></div></div>' +
'</div>';
}).join('');
}
onMounted(() => {
render();
});
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.bt-card{background:#fff;border-radius:18px;padding:22px;width:100%;max-width:420px;box-shadow:0 18px 44px rgba(15,23,42,.1)}
.bt-head{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:14px}
.bt-head h3{font-size:16px;font-weight:800;color:#0f172a}
.bt-head p{font-size:12px;color:#94a3b8;margin-top:2px}
.bt-total{text-align:right}
.bt-total span{font-size:18px;font-weight:800;color:#0f172a;font-variant-numeric:tabular-nums}
.bt-of{font-size:12px;font-weight:600;color:#94a3b8}
.bt-overall{margin-bottom:18px}
.bt-overall-track{height:9px;background:#f1f5f9;border-radius:999px;overflow:hidden}
.bt-overall-fill{height:100%;width:0;border-radius:999px;background:linear-gradient(90deg,#6366f1,#8b5cf6);transition:width .6s cubic-bezier(.4,0,.2,1)}
.bt-overall-fill.warn{background:linear-gradient(90deg,#f59e0b,#f97316)}
.bt-overall-fill.over{background:linear-gradient(90deg,#ef4444,#dc2626)}
.bt-overall-label{display:block;margin-top:6px;font-size:11.5px;font-weight:600;color:#64748b}
.bt-cats{display:flex;flex-direction:column;gap:13px}
.bt-cat-row{display:flex;flex-direction:column;gap:5px}
.bt-cat-top{display:flex;align-items:center;justify-content:space-between;font-size:13px}
.bt-cat-name{display:flex;align-items:center;gap:7px;font-weight:700;color:#1e293b}
.bt-cat-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.bt-cat-amt{font-weight:700;color:#475569;font-variant-numeric:tabular-nums;font-size:12.5px}
.bt-cat-amt .bt-cap{color:#cbd5e1;font-weight:600}
.bt-cat-track{height:6px;background:#f1f5f9;border-radius:999px;overflow:hidden}
.bt-cat-fill{height:100%;border-radius:999px;transition:width .6s cubic-bezier(.4,0,.2,1)}
.bt-cat-row.over .bt-cat-amt{color:#dc2626}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-budget-tracker-card',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="bt-card">
<div class="bt-head">
<div>
<h3>Monthly budget</h3>
<p>June 2026</p>
</div>
<div class="bt-total">
<span id="btSpent">$0</span> <span class="bt-of">of $2,400</span>
</div>
</div>
<div class="bt-overall">
<div class="bt-overall-track"><div class="bt-overall-fill" id="btOverallFill"></div></div>
<span class="bt-overall-label" id="btOverallLabel"></span>
</div>
<div class="bt-cats" id="btCats"></div>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.bt-card{background:#fff;border-radius:18px;padding:22px;width:100%;max-width:420px;box-shadow:0 18px 44px rgba(15,23,42,.1)}
.bt-head{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:14px}
.bt-head h3{font-size:16px;font-weight:800;color:#0f172a}
.bt-head p{font-size:12px;color:#94a3b8;margin-top:2px}
.bt-total{text-align:right}
.bt-total span{font-size:18px;font-weight:800;color:#0f172a;font-variant-numeric:tabular-nums}
.bt-of{font-size:12px;font-weight:600;color:#94a3b8}
.bt-overall{margin-bottom:18px}
.bt-overall-track{height:9px;background:#f1f5f9;border-radius:999px;overflow:hidden}
.bt-overall-fill{height:100%;width:0;border-radius:999px;background:linear-gradient(90deg,#6366f1,#8b5cf6);transition:width .6s cubic-bezier(.4,0,.2,1)}
.bt-overall-fill.warn{background:linear-gradient(90deg,#f59e0b,#f97316)}
.bt-overall-fill.over{background:linear-gradient(90deg,#ef4444,#dc2626)}
.bt-overall-label{display:block;margin-top:6px;font-size:11.5px;font-weight:600;color:#64748b}
.bt-cats{display:flex;flex-direction:column;gap:13px}
.bt-cat-row{display:flex;flex-direction:column;gap:5px}
.bt-cat-top{display:flex;align-items:center;justify-content:space-between;font-size:13px}
.bt-cat-name{display:flex;align-items:center;gap:7px;font-weight:700;color:#1e293b}
.bt-cat-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.bt-cat-amt{font-weight:700;color:#475569;font-variant-numeric:tabular-nums;font-size:12.5px}
.bt-cat-amt .bt-cap{color:#cbd5e1;font-weight:600}
.bt-cat-track{height:6px;background:#f1f5f9;border-radius:999px;overflow:hidden}
.bt-cat-fill{height:100%;border-radius:999px;transition:width .6s cubic-bezier(.4,0,.2,1)}
.bt-cat-row.over .bt-cat-amt{color:#dc2626}
`]
})
export class BudgetTrackerCardComponent implements AfterViewInit {
ngAfterViewInit(): void {
var CATEGORIES = [
{ name: 'Groceries', color: '#22c55e', spent: 380, cap: 500 },
{ name: 'Dining out', color: '#f59e0b', spent: 265, cap: 250 },
{ name: 'Transport', color: '#6366f1', spent: 140, cap: 300 },
{ name: 'Entertainment', color: '#ec4899', spent: 95, cap: 150 },
{ name: 'Shopping', color: '#0ea5e9', spent: 410, cap: 400 },
];
var BUDGET_CAP = 2400;
function render() {
var totalSpent = CATEGORIES.reduce(function (s, c) { return s + c.spent; }, 0);
document.getElementById('btSpent').textContent = '$' + totalSpent.toLocaleString();
var pct = Math.min(100, (totalSpent / BUDGET_CAP) * 100);
var overallFill = document.getElementById('btOverallFill');
overallFill.style.width = pct + '%';
overallFill.className = 'bt-overall-fill' + (totalSpent > BUDGET_CAP ? ' over' : pct > 80 ? ' warn' : '');
var remaining = BUDGET_CAP - totalSpent;
document.getElementById('btOverallLabel').textContent = remaining >= 0
? '$' + remaining.toLocaleString() + ' left this month (' + Math.round(pct) + '% used)'
: '$' + Math.abs(remaining).toLocaleString() + ' over budget';
var cats = document.getElementById('btCats');
cats.innerHTML = CATEGORIES.map(function (c) {
var catPct = Math.min(100, (c.spent / c.cap) * 100);
var over = c.spent > c.cap;
return '<div class="bt-cat-row' + (over ? ' over' : '') + '">' +
'<div class="bt-cat-top">' +
'<span class="bt-cat-name"><i class="bt-cat-dot" style="background:' + c.color + '"></i>' + c.name + '</span>' +
'<span class="bt-cat-amt">$' + c.spent + ' <span class="bt-cap">/ $' + c.cap + '</span></span>' +
'</div>' +
'<div class="bt-cat-track"><div class="bt-cat-fill" style="width:' + catPct + '%;background:' + (over ? '#ef4444' : c.color) + '"></div></div>' +
'</div>';
}).join('');
}
render();
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { render });
}
}