Coworking — Credits Balance
A warm, industrial members-area widget that shows a coworking member's monthly credits at a glance. An animated circular meter tracks remaining versus plan, a breakdown panel splits the allowance across meeting rooms, print units, guest passes and locker hours, and a filterable usage history lists recent spends and top-ups. A top-up modal lets members pick a credit bundle and instantly raises the balance, with toast confirmation. Built with plain HTML, CSS and vanilla JS.
MCP
Code
:root {
--concrete: #efeae3;
--concrete-d: #e2dcd2;
--amber: #e8902b;
--amber-d: #cc7918;
--amber-50: #fdf1e2;
--char: #1c1b19;
--ink: #26241f;
--ink-2: #4a463e;
--muted: #7b766c;
--bg: #f6f3ee;
--surface: #ffffff;
--plant: #5f7a52;
--line: rgba(28, 27, 25, 0.1);
--line-2: rgba(28, 27, 25, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--occupied: #d4503e;
--free: #2f9e6f;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow: 0 1px 2px rgba(28, 27, 25, 0.05), 0 8px 24px rgba(28, 27, 25, 0.06);
--shadow-lg: 0 18px 48px rgba(28, 27, 25, 0.18);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h2 { margin: 0; font-size: 1.05rem; font-weight: 700; letter-spacing: -0.01em; color: var(--char); }
p { margin: 0; }
.wrap {
max-width: 1080px;
margin: 0 auto;
padding: 28px 22px 56px;
}
/* Topbar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding-bottom: 22px;
margin-bottom: 22px;
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
width: 42px; height: 42px;
display: grid; place-items: center;
background: var(--char); color: var(--concrete);
border-radius: 12px; font-size: 1.25rem;
}
.brand-name { font-weight: 800; letter-spacing: -0.02em; color: var(--char); }
.brand-sub { font-size: .8rem; color: var(--muted); }
.member { display: flex; align-items: center; gap: 11px; }
.avatar {
width: 40px; height: 40px; border-radius: 50%;
display: grid; place-items: center;
background: linear-gradient(135deg, var(--amber), var(--amber-d));
color: #fff; font-weight: 700; font-size: .85rem;
}
.member-name { font-weight: 600; font-size: .9rem; color: var(--char); }
.member-plan { font-size: .78rem; color: var(--muted); }
/* Grid */
.grid {
display: grid;
grid-template-columns: 1.15fr .85fr;
grid-template-areas: "meter breakdown" "history history";
gap: 18px;
}
.card-meter { grid-area: meter; }
.card-breakdown { grid-area: breakdown; }
.card-history { grid-area: history; }
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px;
box-shadow: var(--shadow);
}
.card-head {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; margin-bottom: 18px;
}
.pill {
font-size: .74rem; font-weight: 600;
padding: 4px 11px; border-radius: 999px;
}
.pill-ok { background: rgba(47, 158, 111, .12); color: var(--ok); }
/* Meter */
.meter-row {
display: flex; align-items: center; gap: 24px;
flex-wrap: wrap; margin-bottom: 22px;
}
.meter { position: relative; width: 188px; height: 188px; flex: none; }
.meter-svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.meter-track {
fill: none; stroke: var(--concrete-d); stroke-width: 16;
}
.meter-fill {
fill: none; stroke: url(#none); stroke: var(--amber);
stroke-width: 16; stroke-linecap: round;
stroke-dasharray: 527.79;
stroke-dashoffset: 527.79;
transition: stroke-dashoffset 1.1s cubic-bezier(.22, 1, .36, 1);
}
.meter-center {
position: absolute; inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center; text-align: center;
}
.meter-num { font-size: 2.6rem; font-weight: 800; color: var(--char); letter-spacing: -0.03em; line-height: 1; }
.meter-of { font-size: .76rem; color: var(--muted); margin-top: 4px; }
.meter-tag {
margin-top: 8px; font-size: .68rem; font-weight: 600;
text-transform: uppercase; letter-spacing: .08em;
color: var(--plant);
}
.legend { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; min-width: 150px; flex: 1; }
.legend li { display: flex; align-items: center; gap: 9px; font-size: .85rem; }
.legend-k { color: var(--ink-2); }
.legend-v { margin-left: auto; font-weight: 700; color: var(--char); }
.dot { width: 11px; height: 11px; border-radius: 4px; flex: none; }
.dot-amber { background: var(--amber); }
.dot-plant { background: var(--plant); }
.dot-line { background: var(--concrete-d); }
/* Buttons */
.btn {
display: inline-flex; align-items: center; justify-content: center;
gap: 7px; width: 100%;
font-family: inherit; font-size: .92rem; font-weight: 600;
padding: 12px 16px; border-radius: var(--r-md);
border: 1px solid transparent; cursor: pointer;
transition: transform .12s ease, background .15s ease, box-shadow .15s ease;
}
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--char); color: var(--concrete); }
.btn-primary:hover { background: #2c2a26; box-shadow: 0 6px 18px rgba(28, 27, 25, .22); }
.btn-ghost { background: transparent; color: var(--ink-2); border-color: var(--line-2); width: auto; }
.btn-ghost:hover { background: var(--concrete); }
.btn:focus-visible { outline: 3px solid rgba(232, 144, 43, .45); outline-offset: 2px; }
/* Breakdown */
.bd-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 18px; }
.bd-item { display: flex; align-items: center; gap: 13px; }
.bd-ico {
width: 38px; height: 38px; flex: none; border-radius: 11px;
display: grid; place-items: center; font-size: 1.05rem;
background: var(--amber-50); color: var(--amber-d);
}
.ico-print { background: rgba(95, 122, 82, .14); color: var(--plant); }
.ico-guest { background: rgba(212, 80, 62, .12); color: var(--danger); }
.ico-locker { background: var(--concrete); color: var(--ink-2); }
.bd-body { flex: 1; min-width: 0; }
.bd-name { font-size: .86rem; font-weight: 600; color: var(--ink); margin-bottom: 7px; }
.bar { height: 7px; background: var(--concrete-d); border-radius: 999px; overflow: hidden; }
.bar-fill {
display: block; height: 100%; width: var(--p, 0%);
background: linear-gradient(90deg, var(--amber), var(--amber-d));
border-radius: 999px;
}
.bd-val { font-size: .9rem; font-weight: 700; color: var(--char); white-space: nowrap; }
.bd-val small { color: var(--muted); font-weight: 500; font-size: .76rem; }
/* History */
.seg { display: inline-flex; background: var(--concrete); border-radius: 999px; padding: 3px; gap: 2px; }
.seg-btn {
font-family: inherit; font-size: .78rem; font-weight: 600;
color: var(--ink-2); background: transparent;
border: none; border-radius: 999px; padding: 6px 13px; cursor: pointer;
transition: background .15s ease, color .15s ease;
}
.seg-btn.is-active { background: var(--surface); color: var(--char); box-shadow: 0 1px 3px rgba(28, 27, 25, .12); }
.seg-btn:focus-visible { outline: 2px solid var(--amber); outline-offset: 1px; }
.hist-list { list-style: none; margin: 0; padding: 0; }
.hist-row {
display: flex; align-items: center; gap: 14px;
padding: 13px 4px;
border-bottom: 1px solid var(--line);
animation: fade .25s ease both;
}
.hist-row:last-child { border-bottom: none; }
@keyframes fade { from { opacity: 0; transform: translateY(4px); } }
.hist-ico {
width: 36px; height: 36px; flex: none; border-radius: 10px;
display: grid; place-items: center; font-size: .95rem;
background: var(--concrete); color: var(--ink-2);
}
.hist-ico.is-topup { background: rgba(47, 158, 111, .13); color: var(--ok); }
.hist-main { flex: 1; min-width: 0; }
.hist-title { font-size: .88rem; font-weight: 600; color: var(--ink); }
.hist-sub { font-size: .77rem; color: var(--muted); }
.hist-amt { font-weight: 700; font-size: .92rem; white-space: nowrap; }
.hist-amt.neg { color: var(--char); }
.hist-amt.pos { color: var(--ok); }
/* Modal */
.modal { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 20px; }
.modal[hidden] { display: none; }
.modal-backdrop { position: absolute; inset: 0; background: rgba(28, 27, 25, .5); backdrop-filter: blur(2px); animation: fade .2s ease; }
.modal-card {
position: relative; width: 100%; max-width: 440px;
background: var(--surface); border-radius: var(--r-lg);
padding: 22px; box-shadow: var(--shadow-lg);
animation: pop .22s cubic-bezier(.22, 1, .36, 1);
}
@keyframes pop { from { opacity: 0; transform: translateY(12px) scale(.97); } }
.icon-btn {
width: 32px; height: 32px; border: none; border-radius: 9px;
background: var(--concrete); color: var(--ink-2); cursor: pointer;
font-size: .85rem;
}
.icon-btn:hover { background: var(--concrete-d); }
.modal-note { font-size: .85rem; color: var(--ink-2); margin-bottom: 16px; }
.bundles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px; }
.bundle {
position: relative; font-family: inherit; cursor: pointer;
display: flex; flex-direction: column; align-items: center; gap: 1px;
padding: 16px 8px; border-radius: var(--r-md);
background: var(--surface); border: 1.5px solid var(--line-2);
transition: border-color .15s ease, background .15s ease, transform .12s ease;
}
.bundle:hover { transform: translateY(-1px); }
.bundle.is-sel { border-color: var(--amber); background: var(--amber-50); }
.bundle:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
.bundle-c { font-size: 1.5rem; font-weight: 800; color: var(--char); letter-spacing: -0.02em; }
.bundle-l { font-size: .72rem; color: var(--muted); }
.bundle-p { margin-top: 5px; font-size: .88rem; font-weight: 700; color: var(--ink); }
.bundle-tag {
position: absolute; top: -9px; left: 50%; transform: translateX(-50%);
font-size: .64rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
background: var(--amber); color: #fff; padding: 2px 9px; border-radius: 999px;
}
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; }
.modal-actions .btn-primary { width: auto; }
/* Toast */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 16px);
background: var(--char); color: var(--concrete);
font-size: .86rem; font-weight: 500;
padding: 12px 18px; border-radius: 999px;
box-shadow: var(--shadow-lg);
opacity: 0; pointer-events: none;
transition: opacity .25s ease, transform .25s ease;
z-index: 60; max-width: calc(100% - 40px); text-align: center;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* Responsive */
@media (max-width: 820px) {
.grid {
grid-template-columns: 1fr;
grid-template-areas: "meter" "breakdown" "history";
}
}
@media (max-width: 520px) {
.wrap { padding: 20px 15px 48px; }
.meter-row { justify-content: center; }
.meter { width: 168px; height: 168px; }
.meter-num { font-size: 2.3rem; }
.bundles { grid-template-columns: 1fr; }
.bundle { flex-direction: row; justify-content: space-between; padding: 14px 16px; }
.bundle-l { margin-right: auto; margin-left: 6px; }
.seg-btn { padding: 6px 10px; }
.card-head { flex-wrap: wrap; }
}(function () {
"use strict";
var PLAN = 240;
var state = {
remaining: 172,
used: 68,
bundle: { credits: 100, price: 72 },
};
var $ = function (sel, root) { return (root || document).querySelector(sel); };
var $$ = function (sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); };
/* ---- Toast helper ---- */
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);
}
/* ---- Animated circular meter ---- */
var CIRC = 2 * Math.PI * 84; // r=84
var fillEl = $("#meterFill");
var numEl = $("#meterNum");
var usedValEl = $("#usedVal");
var remainValEl = $("#remainVal");
var meterImg = $(".meter");
function setMeter(remaining, animate) {
state.remaining = remaining;
state.used = PLAN - remaining;
var pct = Math.max(0, Math.min(1, remaining / PLAN));
var offset = CIRC * (1 - pct);
// color shifts toward warning/danger as balance drops
var color = pct > 0.4 ? "var(--amber)" : pct > 0.15 ? "var(--warn)" : "var(--danger)";
fillEl.style.stroke = color;
if (animate) {
// force reflow so the dash transition re-runs
fillEl.style.strokeDashoffset = CIRC;
void fillEl.getBoundingClientRect();
}
fillEl.style.strokeDashoffset = offset;
usedValEl.textContent = state.used;
remainValEl.textContent = remaining;
if (meterImg) meterImg.setAttribute("aria-label", remaining + " of " + PLAN + " monthly credits remaining");
animateNumber(remaining);
}
function animateNumber(target) {
var start = parseInt(numEl.textContent, 10) || 0;
var dur = 1100, t0 = null;
function step(ts) {
if (!t0) t0 = ts;
var k = Math.min(1, (ts - t0) / dur);
var eased = 1 - Math.pow(1 - k, 3);
numEl.textContent = Math.round(start + (target - start) * eased);
if (k < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
/* ---- Usage history ---- */
var history = [
{ type: "spend", icon: "▤", title: "Boardroom — 2 hrs", sub: "Yesterday · 14:00", amt: -8 },
{ type: "topup", icon: "+", title: "Top-up · 100 credits", sub: "Jun 14 · Card •••• 4821", amt: 100 },
{ type: "spend", icon: "⎙", title: "Color print batch", sub: "Jun 13 · 22 pages", amt: -11 },
{ type: "spend", icon: "☻", title: "Guest pass — D. Vance", sub: "Jun 12 · Front desk", amt: -6 },
{ type: "spend", icon: "▤", title: "Focus pod — 90 min", sub: "Jun 10 · 09:30", amt: -5 },
{ type: "topup", icon: "+", title: "Monthly plan reset", sub: "Jun 1 · Flex Studio", amt: 240 },
{ type: "spend", icon: "▦", title: "Locker hold — overnight", sub: "May 30 · 16 hrs", amt: -4 },
];
var listEl = $("#histList");
function renderHistory(filter) {
listEl.innerHTML = "";
history
.filter(function (h) { return filter === "all" || filter === h.type; })
.forEach(function (h) {
var li = document.createElement("li");
li.className = "hist-row";
var pos = h.amt > 0;
li.innerHTML =
'<span class="hist-ico' + (pos ? " is-topup" : "") + '" aria-hidden="true">' + h.icon + "</span>" +
'<div class="hist-main">' +
'<p class="hist-title">' + h.title + "</p>" +
'<p class="hist-sub">' + h.sub + "</p>" +
"</div>" +
'<span class="hist-amt ' + (pos ? "pos" : "neg") + '">' + (pos ? "+" : "") + h.amt + "</span>";
listEl.appendChild(li);
});
if (!listEl.children.length) {
var empty = document.createElement("li");
empty.className = "hist-row";
empty.innerHTML = '<div class="hist-main"><p class="hist-sub">No entries for this filter.</p></div>';
listEl.appendChild(empty);
}
}
$$(".seg-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
$$(".seg-btn").forEach(function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-selected", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-selected", "true");
renderHistory(btn.dataset.filter);
});
});
/* ---- Top-up modal ---- */
var modal = $("#topupModal");
var confirmBtn = $("#confirmTopup");
var lastFocus = null;
function openModal() {
lastFocus = document.activeElement;
modal.hidden = false;
var sel = $(".bundle.is-sel", modal) || $(".bundle", modal);
if (sel) sel.focus();
document.addEventListener("keydown", onKeydown);
}
function closeModal() {
modal.hidden = true;
document.removeEventListener("keydown", onKeydown);
if (lastFocus) lastFocus.focus();
}
function onKeydown(e) { if (e.key === "Escape") closeModal(); }
$("#topupBtn").addEventListener("click", openModal);
$$("[data-close]", modal).forEach(function (el) { el.addEventListener("click", closeModal); });
function updateConfirm() {
confirmBtn.textContent = "Add " + state.bundle.credits + " credits · $" + state.bundle.price;
}
$$(".bundle", modal).forEach(function (b) {
b.addEventListener("click", function () {
$$(".bundle", modal).forEach(function (o) {
o.classList.remove("is-sel");
o.setAttribute("aria-checked", "false");
});
b.classList.add("is-sel");
b.setAttribute("aria-checked", "true");
state.bundle = { credits: parseInt(b.dataset.credits, 10), price: parseInt(b.dataset.price, 10) };
updateConfirm();
});
});
confirmBtn.addEventListener("click", function () {
var added = state.bundle.credits;
var capped = Math.min(PLAN, state.remaining + added);
closeModal();
history.unshift({
type: "topup",
icon: "+",
title: "Top-up · " + added + " credits",
sub: "Just now · Card •••• 4821",
amt: added,
});
var active = $(".seg-btn.is-active");
renderHistory(active ? active.dataset.filter : "all");
setMeter(capped, true);
toast("Added " + added + " credits — balance " + capped + "/" + PLAN);
});
/* ---- Init ---- */
updateConfirm();
renderHistory("all");
requestAnimationFrame(function () {
requestAnimationFrame(function () { setMeter(state.remaining, true); });
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coworking — Credits Balance</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=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap" role="main">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◷</span>
<div>
<p class="brand-name">Foundry & Fern</p>
<p class="brand-sub">Studio · Members area</p>
</div>
</div>
<div class="member">
<div class="avatar" aria-hidden="true">MR</div>
<div class="member-meta">
<p class="member-name">Mara Okonkwo</p>
<p class="member-plan">Flex Studio · renews Jul 1</p>
</div>
</div>
</header>
<section class="grid">
<!-- Balance meter card -->
<article class="card card-meter" aria-labelledby="meter-h">
<div class="card-head">
<h2 id="meter-h">Credits balance</h2>
<span class="pill pill-ok" id="cyclePill">18 days left</span>
</div>
<div class="meter-row">
<div class="meter" role="img" aria-label="172 of 240 monthly credits remaining">
<svg viewBox="0 0 200 200" class="meter-svg" aria-hidden="true">
<circle class="meter-track" cx="100" cy="100" r="84" />
<circle class="meter-fill" id="meterFill" cx="100" cy="100" r="84" />
</svg>
<div class="meter-center">
<span class="meter-num" id="meterNum">0</span>
<span class="meter-of">of 240 credits</span>
<span class="meter-tag" id="meterTag">remaining</span>
</div>
</div>
<ul class="legend">
<li>
<span class="dot dot-amber" aria-hidden="true"></span>
<span class="legend-k">Used this cycle</span>
<span class="legend-v" id="usedVal">68</span>
</li>
<li>
<span class="dot dot-plant" aria-hidden="true"></span>
<span class="legend-k">Remaining</span>
<span class="legend-v" id="remainVal">172</span>
</li>
<li>
<span class="dot dot-line" aria-hidden="true"></span>
<span class="legend-k">Monthly plan</span>
<span class="legend-v">240</span>
</li>
</ul>
</div>
<button class="btn btn-primary" id="topupBtn" type="button">
<span aria-hidden="true">+</span> Top up credits
</button>
</article>
<!-- Breakdown card -->
<article class="card card-breakdown" aria-labelledby="bd-h">
<div class="card-head">
<h2 id="bd-h">Allowance breakdown</h2>
</div>
<ul class="bd-list">
<li class="bd-item">
<span class="bd-ico ico-room" aria-hidden="true">▤</span>
<div class="bd-body">
<p class="bd-name">Meeting room credits</p>
<div class="bar"><span class="bar-fill" style="--p:64%"></span></div>
</div>
<span class="bd-val">16<small>/25</small></span>
</li>
<li class="bd-item">
<span class="bd-ico ico-print" aria-hidden="true">⎙</span>
<div class="bd-body">
<p class="bd-name">Print & scan units</p>
<div class="bar"><span class="bar-fill" style="--p:38%"></span></div>
</div>
<span class="bd-val">76<small>/200</small></span>
</li>
<li class="bd-item">
<span class="bd-ico ico-guest" aria-hidden="true">☻</span>
<div class="bd-body">
<p class="bd-name">Guest day passes</p>
<div class="bar"><span class="bar-fill" style="--p:80%"></span></div>
</div>
<span class="bd-val">4<small>/5</small></span>
</li>
<li class="bd-item">
<span class="bd-ico ico-locker" aria-hidden="true">▦</span>
<div class="bd-body">
<p class="bd-name">Locker hours</p>
<div class="bar"><span class="bar-fill" style="--p:21%"></span></div>
</div>
<span class="bd-val">152<small>/720</small></span>
</li>
</ul>
</article>
<!-- Usage history card -->
<article class="card card-history" aria-labelledby="hist-h">
<div class="card-head">
<h2 id="hist-h">Usage history</h2>
<div class="seg" role="tablist" aria-label="Filter usage">
<button class="seg-btn is-active" role="tab" aria-selected="true" data-filter="all" type="button">All</button>
<button class="seg-btn" role="tab" aria-selected="false" data-filter="spend" type="button">Spent</button>
<button class="seg-btn" role="tab" aria-selected="false" data-filter="topup" type="button">Top-ups</button>
</div>
</div>
<ul class="hist-list" id="histList"><!-- rows injected by script.js --></ul>
</article>
</section>
</main>
<!-- Top-up modal -->
<div class="modal" id="topupModal" role="dialog" aria-modal="true" aria-labelledby="modal-h" hidden>
<div class="modal-backdrop" data-close></div>
<div class="modal-card" role="document">
<div class="card-head">
<h2 id="modal-h">Top up credits</h2>
<button class="icon-btn" data-close type="button" aria-label="Close">✕</button>
</div>
<p class="modal-note">Bundles are added instantly and never expire while your plan is active.</p>
<div class="bundles" role="radiogroup" aria-label="Choose a top-up bundle">
<button class="bundle" role="radio" aria-checked="false" data-credits="40" data-price="32" type="button">
<span class="bundle-c">40</span><span class="bundle-l">credits</span><span class="bundle-p">$32</span>
</button>
<button class="bundle is-sel" role="radio" aria-checked="true" data-credits="100" data-price="72" type="button">
<span class="bundle-tag">Popular</span>
<span class="bundle-c">100</span><span class="bundle-l">credits</span><span class="bundle-p">$72</span>
</button>
<button class="bundle" role="radio" aria-checked="false" data-credits="250" data-price="160" type="button">
<span class="bundle-c">250</span><span class="bundle-l">credits</span><span class="bundle-p">$160</span>
</button>
</div>
<div class="modal-actions">
<button class="btn btn-ghost" data-close type="button">Cancel</button>
<button class="btn btn-primary" id="confirmTopup" type="button">Add 100 credits · $72</button>
</div>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Credits Balance
A self-contained members-area panel for a fictional coworking studio, Foundry & Fern. The hero card is an animated circular meter that counts up to the member’s remaining monthly credits, with a legend breaking out used, remaining and plan totals. The stroke colour shifts from amber to warning to danger as the balance runs low, so members can read their status without parsing numbers.
Alongside the meter, an allowance breakdown lists how each credit pool is being spent — meeting rooms, print and scan units, guest day passes and locker hours — each with a progress bar. A usage history list shows recent activity and can be filtered to all entries, spends only, or top-ups only via a segmented control.
Tapping Top up credits opens an accessible modal with three selectable bundles. Confirming a bundle animates the meter up to the new balance, prepends a fresh entry to the history, and fires a toast. The modal traps Escape-to-close, restores focus, and the whole layout reflows cleanly down to a 360px phone width.
Illustrative UI only — fictional coworking space, not a real booking system.