Banking — Transactions
A trust-first fintech transaction history screen with a navy gradient account card, available balance, money-in and money-out totals, and a searchable ledger grouped by day. Rows show category icons, credit-green and debit-ink amounts, a running balance, and pending, cleared or failed status pills. Filter by type, category and date range, expand any row for reference and method detail, and export the filtered view with a toast. Pure HTML, CSS and vanilla JavaScript.
MCP
Code
:root {
--navy: #0e1b3a;
--navy-2: #16264d;
--ink: #0e1726;
--ink-2: #3a4660;
--muted: #697089;
--accent: #3b6ef6;
--accent-d: #2a55cc;
--accent-50: #eaf0ff;
--teal: #0fb5a6;
--violet: #7c5cff;
--bg: #f5f7fb;
--surface: #ffffff;
--line: rgba(14, 27, 58, 0.1);
--line-2: rgba(14, 27, 58, 0.18);
--ok: #1f9d62;
--warn: #d9982b;
--danger: #d4493e;
--credit: #1f9d62;
--debit: #0e1726;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(14, 27, 58, 0.06), 0 1px 3px rgba(14, 27, 58, 0.05);
--sh-2: 0 6px 18px rgba(14, 27, 58, 0.08), 0 2px 6px rgba(14, 27, 58, 0.05);
--sh-3: 0 18px 48px rgba(14, 27, 58, 0.14);
}
* {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
}
.amount {
font-variant-numeric: tabular-nums;
letter-spacing: -0.01em;
}
.app {
max-width: 960px;
margin: 0 auto;
padding: 18px 20px 56px;
}
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 6px 2px 18px;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.logo {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: var(--r-md);
color: #fff;
background: linear-gradient(150deg, var(--navy-2), var(--navy));
box-shadow: var(--sh-2);
}
.brand-meta {
display: flex;
flex-direction: column;
line-height: 1.25;
}
.brand-meta strong {
font-weight: 700;
font-size: 0.98rem;
}
.verified {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.72rem;
font-weight: 600;
color: var(--ok);
}
.topbar-right {
display: flex;
align-items: center;
gap: 12px;
}
.avatar {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: 50%;
font-size: 0.78rem;
font-weight: 700;
color: #fff;
background: linear-gradient(150deg, var(--violet), var(--accent));
box-shadow: var(--sh-1);
}
.ghost-btn {
display: inline-flex;
align-items: center;
gap: 7px;
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink);
font: inherit;
font-weight: 600;
font-size: 0.84rem;
padding: 9px 14px;
border-radius: 999px;
cursor: pointer;
box-shadow: var(--sh-1);
transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease;
}
.ghost-btn:hover {
border-color: var(--accent);
color: var(--accent-d);
box-shadow: var(--sh-2);
}
.ghost-btn:active {
transform: translateY(1px);
}
.ghost-btn:focus-visible,
.link-btn:focus-visible,
.select select:focus-visible,
.search input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* ---------- Layout ---------- */
.layout {
display: grid;
gap: 18px;
}
/* ---------- Account card ---------- */
.account-card {
position: relative;
overflow: hidden;
padding: 22px;
border-radius: var(--r-lg);
color: #fff;
background: radial-gradient(140% 120% at 100% 0%, var(--navy-2), var(--navy));
box-shadow: var(--sh-3);
}
.account-card::after {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(50% 60% at 90% 10%, rgba(124, 92, 255, 0.35), transparent 60%),
radial-gradient(40% 50% at 10% 100%, rgba(15, 181, 166, 0.28), transparent 60%);
pointer-events: none;
}
.account-card > * {
position: relative;
z-index: 1;
}
.ac-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.ac-label {
font-size: 0.78rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.72);
}
.ac-num {
font-variant-numeric: tabular-nums;
font-weight: 700;
font-size: 1.02rem;
letter-spacing: 0.04em;
margin-top: 2px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 0.72rem;
font-weight: 600;
padding: 5px 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
color: #fff;
backdrop-filter: blur(4px);
}
.ac-balance {
margin-top: 18px;
}
.ac-balance-label {
display: block;
font-size: 0.76rem;
color: rgba(255, 255, 255, 0.7);
}
.ac-balance .amount {
font-size: 2rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.ac-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.14);
}
.ac-stat-label {
display: block;
font-size: 0.72rem;
color: rgba(255, 255, 255, 0.66);
}
.ac-stats .amount {
font-size: 1.05rem;
font-weight: 700;
}
.ac-stats .credit {
color: #7ff0c0;
}
.ac-stats .debit {
color: #fff;
}
/* ---------- Ledger ---------- */
.ledger {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-2);
padding: 18px;
}
.controls {
display: grid;
gap: 12px;
}
.search {
display: flex;
align-items: center;
gap: 9px;
padding: 0 14px;
border: 1px solid var(--line-2);
border-radius: var(--r-md);
background: var(--bg);
color: var(--muted);
transition: border-color 0.12s ease, background 0.12s ease;
}
.search:focus-within {
border-color: var(--accent);
background: var(--surface);
}
.search input {
flex: 1;
border: 0;
background: transparent;
font: inherit;
font-size: 0.92rem;
color: var(--ink);
padding: 12px 0;
}
.search input:focus-visible {
outline: none;
}
.filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.select {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
min-width: 130px;
}
.select span {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
}
.select select {
appearance: none;
font: inherit;
font-size: 0.86rem;
font-weight: 500;
color: var(--ink);
padding: 9px 30px 9px 12px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--surface)
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23697089' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")
no-repeat right 10px center;
cursor: pointer;
}
.select select:hover {
border-color: var(--accent);
}
.ledger-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 18px 2px 8px;
font-size: 0.8rem;
color: var(--muted);
font-weight: 500;
}
.link-btn {
border: 0;
background: none;
font: inherit;
font-weight: 600;
font-size: 0.8rem;
color: var(--accent-d);
cursor: pointer;
padding: 2px 4px;
border-radius: 6px;
}
.link-btn:hover {
text-decoration: underline;
}
/* ---------- Transaction list ---------- */
.tx-list {
display: flex;
flex-direction: column;
}
.day-group {
margin-top: 8px;
}
.day-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
padding: 12px 6px 6px;
position: sticky;
top: 0;
background: linear-gradient(var(--surface) 70%, transparent);
}
.day-label {
font-size: 0.74rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ink-2);
}
.day-total {
font-size: 0.74rem;
font-weight: 600;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.tx {
border-top: 1px solid var(--line);
}
.tx-row {
display: grid;
grid-template-columns: 42px 1fr auto;
align-items: center;
gap: 12px;
width: 100%;
text-align: left;
border: 0;
background: none;
font: inherit;
color: inherit;
padding: 12px 8px;
border-radius: var(--r-md);
cursor: pointer;
transition: background 0.12s ease;
}
.tx-row:hover {
background: var(--accent-50);
}
.tx-row:active {
background: #e1e9ff;
}
.tx-icon {
display: grid;
place-items: center;
width: 42px;
height: 42px;
border-radius: 12px;
font-size: 1.05rem;
background: var(--bg);
border: 1px solid var(--line);
}
.tx-main {
min-width: 0;
}
.tx-merchant {
font-weight: 600;
font-size: 0.92rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tx-sub {
display: flex;
align-items: center;
gap: 7px;
margin-top: 2px;
font-size: 0.76rem;
color: var(--muted);
}
.tx-sub .cat {
white-space: nowrap;
}
.tx-amounts {
text-align: right;
white-space: nowrap;
}
.tx-amount {
font-weight: 700;
font-size: 0.96rem;
font-variant-numeric: tabular-nums;
}
.tx-amount.credit {
color: var(--credit);
}
.tx-amount.debit {
color: var(--debit);
}
.tx-balance {
font-size: 0.72rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
margin-top: 2px;
}
.pill {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.68rem;
font-weight: 600;
padding: 2px 8px;
border-radius: 999px;
line-height: 1.4;
}
.pill::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.pill.cleared {
color: var(--ok);
background: rgba(31, 157, 98, 0.12);
}
.pill.pending {
color: var(--warn);
background: rgba(217, 152, 43, 0.14);
}
.pill.failed {
color: var(--danger);
background: rgba(212, 73, 62, 0.12);
}
/* ---------- Expand detail ---------- */
.tx-detail {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.28s ease;
}
.tx.open .tx-detail {
grid-template-rows: 1fr;
}
.tx-detail-inner {
overflow: hidden;
}
.detail-card {
margin: 0 8px 12px 62px;
padding: 14px 16px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 18px;
}
.detail-grid dt {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
color: var(--muted);
}
.detail-grid dd {
margin: 2px 0 0;
font-size: 0.86rem;
font-weight: 600;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.detail-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.detail-actions .ghost-btn {
font-size: 0.78rem;
padding: 7px 12px;
}
/* ---------- Empty ---------- */
.empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 44px 16px;
text-align: center;
color: var(--muted);
}
.empty p {
margin: 0;
font-weight: 500;
}
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 50;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 9px;
padding: 11px 16px;
background: var(--navy);
color: #fff;
font-size: 0.85rem;
font-weight: 500;
border-radius: 999px;
box-shadow: var(--sh-3);
opacity: 0;
transform: translateY(10px);
transition: opacity 0.22s ease, transform 0.22s ease;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
.toast svg {
color: var(--teal);
flex: none;
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.app {
padding: 14px 14px 48px;
}
.brand-meta strong {
font-size: 0.9rem;
}
.ghost-btn span,
.topbar .ghost-btn {
font-size: 0.8rem;
}
.ac-balance .amount {
font-size: 1.7rem;
}
.ac-stats {
gap: 8px;
}
.ac-stats .amount {
font-size: 0.92rem;
}
.select {
min-width: calc(50% - 5px);
}
.tx-row {
grid-template-columns: 38px 1fr auto;
gap: 10px;
padding: 11px 4px;
}
.tx-icon {
width: 38px;
height: 38px;
}
.detail-card {
margin-left: 8px;
}
.detail-grid {
grid-template-columns: 1fr 1fr;
}
.day-head {
padding-left: 4px;
padding-right: 4px;
}
}(function () {
"use strict";
// ---- Fictional data ---------------------------------------------------
const CATEGORIES = {
groceries: { label: "Groceries", icon: "🛒" },
dining: { label: "Dining", icon: "🍽️" },
transport: { label: "Transport", icon: "🚇" },
income: { label: "Income", icon: "💼" },
transfer: { label: "Transfers", icon: "🔁" },
bills: { label: "Bills & Utilities", icon: "💡" },
shopping: { label: "Shopping", icon: "🛍️" },
health: { label: "Health", icon: "➕" },
entertainment: { label: "Entertainment", icon: "🎬" },
};
// Newest first. Amounts: positive = credit, negative = debit.
const RAW = [
{ id: "TX-9041", merchant: "Bluewater Grocers", note: "Card purchase", cat: "groceries", amount: -72.4, day: 0, status: "pending", method: "Visa Debit •••• 4242", ref: "AUTH 8841" },
{ id: "TX-9038", merchant: "Acme Studio Ltd", note: "Salary — June", cat: "income", amount: 4250.0, day: 0, status: "cleared", method: "SEPA Credit", ref: "PAYROLL-06" },
{ id: "TX-9035", merchant: "Metro Transit", note: "Monthly pass", cat: "transport", amount: -56.0, day: 0, status: "cleared", method: "Visa Debit •••• 4242", ref: "AUTH 8810" },
{ id: "TX-9030", merchant: "Lumen Energy", note: "Electricity bill", cat: "bills", amount: -94.18, day: 1, status: "cleared", method: "Direct Debit", ref: "DD-LUMEN" },
{ id: "TX-9026", merchant: "Trattoria Sole", note: "Dinner", cat: "dining", amount: -48.9, day: 1, status: "cleared", method: "Visa Debit •••• 4242", ref: "AUTH 8770" },
{ id: "TX-9021", merchant: "J. Okafor", note: "Split — rent", cat: "transfer", amount: 320.0, day: 1, status: "cleared", method: "Instant transfer", ref: "P2P-5521" },
{ id: "TX-9015", merchant: "Halcyon Pharmacy", note: "Prescription", cat: "health", amount: -23.5, day: 3, status: "cleared", method: "Visa Debit •••• 4242", ref: "AUTH 8612" },
{ id: "TX-9012", merchant: "Streamline+", note: "Subscription", cat: "entertainment", amount: -13.99, day: 3, status: "failed", method: "Visa Debit •••• 4242", ref: "DECLINED" },
{ id: "TX-9008", merchant: "Northfield Market", note: "Weekly shop", cat: "groceries", amount: -88.62, day: 4, status: "cleared", method: "Visa Debit •••• 4242", ref: "AUTH 8540" },
{ id: "TX-9001", merchant: "Drift Outfitters", note: "Running shoes", cat: "shopping", amount: -119.0, day: 6, status: "cleared", method: "Visa Debit •••• 4242", ref: "AUTH 8488" },
{ id: "TX-8994", merchant: "Vale Coffee Co.", note: "Cafe", cat: "dining", amount: -6.2, day: 9, status: "cleared", method: "Visa Debit •••• 4242", ref: "AUTH 8421" },
{ id: "TX-8987", merchant: "Brightline Rail", note: "Refund — delay", cat: "transport", amount: 18.0, day: 12, status: "cleared", method: "Visa Debit •••• 4242", ref: "REF-2210" },
{ id: "TX-8980", merchant: "Aurora Mobile", note: "Phone plan", cat: "bills", amount: -29.0, day: 18, status: "cleared", method: "Direct Debit", ref: "DD-AURORA" },
{ id: "TX-8972", merchant: "Tideline Freelance", note: "Invoice #112", cat: "income", amount: 640.0, day: 24, status: "cleared", method: "SEPA Credit", ref: "INV-112" },
{ id: "TX-8965", merchant: "Cedar Books", note: "Order", cat: "shopping", amount: -34.75, day: 41, status: "cleared", method: "Visa Debit •••• 4242", ref: "AUTH 8190" },
];
const STARTING_BALANCE = 6840.55; // current available balance (today)
// Build derived transaction objects with date + running balance.
const now = new Date();
let running = STARTING_BALANCE;
const TX = RAW.map((t) => {
const date = new Date(now);
date.setHours(11, 5, 0, 0);
date.setDate(now.getDate() - t.day);
const balanceAfter = running;
// The balance BEFORE this txn = balanceAfter - amount (we walk backwards in time).
running = balanceAfter - t.amount;
return Object.assign({}, t, { date, balanceAfter });
});
// ---- Helpers ----------------------------------------------------------
const fmtMoney = (n) =>
(n < 0 ? "-" : "") +
"$" +
Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const signedMoney = (n) =>
(n >= 0 ? "+" : "-") +
"$" +
Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const startOfDay = (d) => {
const x = new Date(d);
x.setHours(0, 0, 0, 0);
return x;
};
function dayLabel(date) {
const t = startOfDay(now).getTime();
const d = startOfDay(date).getTime();
const diff = Math.round((t - d) / 86400000);
if (diff === 0) return "Today";
if (diff === 1) return "Yesterday";
return date.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" });
}
const STATUS_LABEL = { cleared: "Cleared", pending: "Pending", failed: "Failed" };
// ---- DOM refs ---------------------------------------------------------
const $ = (id) => document.getElementById(id);
const listEl = $("txList");
const emptyEl = $("emptyState");
const searchEl = $("searchInput");
const typeEl = $("typeFilter");
const catEl = $("categoryFilter");
const rangeEl = $("rangeFilter");
const countEl = $("resultCount");
const clearBtn = $("clearBtn");
const toastWrap = $("toastWrap");
let openId = null;
// Populate category filter.
Object.keys(CATEGORIES).forEach((key) => {
const opt = document.createElement("option");
opt.value = key;
opt.textContent = CATEGORIES[key].label;
catEl.appendChild(opt);
});
// ---- Summary ----------------------------------------------------------
function renderSummary() {
let inSum = 0,
outSum = 0,
pending = 0;
TX.forEach((t) => {
if (t.amount >= 0) inSum += t.amount;
else outSum += t.amount;
if (t.status === "pending") pending += 1;
});
$("availBalance").textContent = fmtMoney(STARTING_BALANCE);
$("statIn").textContent = "+" + fmtMoney(inSum);
$("statOut").textContent = fmtMoney(outSum);
$("statPending").textContent = String(pending);
}
// ---- Filtering --------------------------------------------------------
function getFiltered() {
const q = searchEl.value.trim().toLowerCase();
const type = typeEl.value;
const cat = catEl.value;
const range = rangeEl.value;
const cutoff =
range === "all" ? null : startOfDay(now).getTime() - (Number(range) - 1) * 86400000;
return TX.filter((t) => {
if (type === "credit" && t.amount < 0) return false;
if (type === "debit" && t.amount >= 0) return false;
if (cat !== "all" && t.cat !== cat) return false;
if (cutoff !== null && startOfDay(t.date).getTime() < cutoff) return false;
if (q) {
const hay = (
t.merchant +
" " +
t.note +
" " +
CATEGORIES[t.cat].label +
" " +
Math.abs(t.amount).toFixed(2)
).toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
});
}
function filtersActive() {
return (
searchEl.value.trim() !== "" ||
typeEl.value !== "all" ||
catEl.value !== "all" ||
rangeEl.value !== "all"
);
}
// ---- Render list ------------------------------------------------------
function render() {
const rows = getFiltered();
listEl.innerHTML = "";
const active = filtersActive();
clearBtn.hidden = !active;
countEl.textContent =
rows.length + " transaction" + (rows.length === 1 ? "" : "s") + (active ? " · filtered" : "");
if (rows.length === 0) {
emptyEl.hidden = false;
return;
}
emptyEl.hidden = true;
// Group by day.
const groups = [];
let current = null;
rows.forEach((t) => {
const key = startOfDay(t.date).getTime();
if (!current || current.key !== key) {
current = { key, label: dayLabel(t.date), items: [], net: 0 };
groups.push(current);
}
current.items.push(t);
current.net += t.amount;
});
const frag = document.createDocumentFragment();
groups.forEach((g) => {
const groupEl = document.createElement("div");
groupEl.className = "day-group";
const head = document.createElement("div");
head.className = "day-head";
head.innerHTML =
'<span class="day-label">' +
g.label +
'</span><span class="day-total">' +
signedMoney(g.net) +
"</span>";
groupEl.appendChild(head);
g.items.forEach((t) => groupEl.appendChild(buildRow(t)));
frag.appendChild(groupEl);
});
listEl.appendChild(frag);
}
function buildRow(t) {
const wrap = document.createElement("div");
wrap.className = "tx" + (t.id === openId ? " open" : "");
wrap.dataset.id = t.id;
const cat = CATEGORIES[t.cat];
const isCredit = t.amount >= 0;
const timeStr = t.date.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
const btn = document.createElement("button");
btn.type = "button";
btn.className = "tx-row";
btn.setAttribute("aria-expanded", t.id === openId ? "true" : "false");
btn.innerHTML =
'<span class="tx-icon" aria-hidden="true">' +
cat.icon +
"</span>" +
'<span class="tx-main">' +
'<span class="tx-merchant">' +
escapeHtml(t.merchant) +
"</span>" +
'<span class="tx-sub">' +
'<span class="cat">' +
cat.label +
"</span><span>·</span>" +
'<span class="pill ' +
t.status +
'">' +
STATUS_LABEL[t.status] +
"</span>" +
"</span>" +
"</span>" +
'<span class="tx-amounts">' +
'<span class="tx-amount ' +
(isCredit ? "credit" : "debit") +
'">' +
signedMoney(t.amount) +
"</span>" +
'<span class="tx-balance">Bal ' +
fmtMoney(t.balanceAfter) +
"</span>" +
"</span>";
btn.addEventListener("click", () => toggle(t.id));
const detail = document.createElement("div");
detail.className = "tx-detail";
detail.innerHTML =
'<div class="tx-detail-inner"><div class="detail-card"><dl class="detail-grid">' +
detailItem("Reference", t.ref) +
detailItem("Method", t.method) +
detailItem("Date & time", t.date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) + " · " + timeStr) +
detailItem("Note", t.note) +
detailItem("Category", cat.label) +
detailItem("Balance after", fmtMoney(t.balanceAfter)) +
"</dl>" +
'<div class="detail-actions">' +
'<button type="button" class="ghost-btn" data-act="receipt">View receipt</button>' +
'<button type="button" class="ghost-btn" data-act="dispute">Report a problem</button>' +
"</div></div></div>";
detail.querySelectorAll("[data-act]").forEach((b) => {
b.addEventListener("click", (e) => {
e.stopPropagation();
const act = b.dataset.act;
toast(act === "receipt" ? "Receipt for " + t.merchant + " sent to your inbox" : "Dispute opened for " + t.id);
});
});
wrap.appendChild(btn);
wrap.appendChild(detail);
return wrap;
}
function detailItem(label, value) {
return "<div><dt>" + label + "</dt><dd>" + escapeHtml(String(value)) + "</dd></div>";
}
function toggle(id) {
openId = openId === id ? null : id;
// Update only affected rows for a smooth expand/collapse.
listEl.querySelectorAll(".tx").forEach((el) => {
const isOpen = el.dataset.id === openId;
el.classList.toggle("open", isOpen);
const b = el.querySelector(".tx-row");
if (b) b.setAttribute("aria-expanded", isOpen ? "true" : "false");
});
}
function escapeHtml(s) {
return s.replace(/[&<>"']/g, (c) =>
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])
);
}
// ---- Toast ------------------------------------------------------------
function toast(msg) {
const el = document.createElement("div");
el.className = "toast";
el.innerHTML =
'<svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true"><path d="M3 8.5 6.4 12 13 4.5" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg><span>' +
escapeHtml(msg) +
"</span>";
toastWrap.appendChild(el);
requestAnimationFrame(() => el.classList.add("show"));
setTimeout(() => {
el.classList.remove("show");
setTimeout(() => el.remove(), 240);
}, 2600);
}
// ---- Export -----------------------------------------------------------
$("exportBtn").addEventListener("click", () => {
const rows = getFiltered();
toast("Exported " + rows.length + " transaction" + (rows.length === 1 ? "" : "s") + " to CSV");
});
// ---- Clear filters ----------------------------------------------------
function clearFilters() {
searchEl.value = "";
typeEl.value = "all";
catEl.value = "all";
rangeEl.value = "all";
render();
}
clearBtn.addEventListener("click", clearFilters);
$("emptyClear").addEventListener("click", clearFilters);
// ---- Wire filter inputs ----------------------------------------------
let searchTimer;
searchEl.addEventListener("input", () => {
clearTimeout(searchTimer);
searchTimer = setTimeout(render, 120);
});
[typeEl, catEl, rangeEl].forEach((el) => el.addEventListener("change", render));
// ---- Init -------------------------------------------------------------
renderSummary();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Banking — Transactions</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>
<div class="app">
<header class="topbar">
<div class="brand">
<span class="logo" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none">
<path d="M12 2 21 7v2H3V7l9-5Z" fill="currentColor" />
<rect x="4" y="10" width="2.5" height="7" rx="0.6" fill="currentColor" />
<rect x="10.75" y="10" width="2.5" height="7" rx="0.6" fill="currentColor" />
<rect x="17.5" y="10" width="2.5" height="7" rx="0.6" fill="currentColor" />
<rect x="3" y="18" width="18" height="2.5" rx="0.6" fill="currentColor" />
</svg>
</span>
<div class="brand-meta">
<strong>Northvale Bank</strong>
<span class="verified">
<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"><path d="M8 1 14 4v4c0 3.5-2.5 6.4-6 7-3.5-.6-6-3.5-6-7V4l6-3Z" fill="currentColor"/><path d="M6.4 8 7.6 9.3 10 6.4" stroke="#fff" stroke-width="1.3" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Verified session
</span>
</div>
</div>
<div class="topbar-right">
<button class="ghost-btn" type="button" id="exportBtn">
<svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true"><path d="M10 3v8m0 0 3-3m-3 3-3-3M4 15h12" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Export CSV
</button>
<div class="avatar" title="Mara Ellison">ME</div>
</div>
</header>
<main class="layout">
<section class="account-card" aria-label="Account summary">
<div class="ac-top">
<div>
<span class="ac-label">Everyday Checking</span>
<div class="ac-num">•••• 4242</div>
</div>
<span class="chip lock">
<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"><rect x="3.5" y="7" width="9" height="6.5" rx="1.4" fill="currentColor"/><path d="M5.5 7V5a2.5 2.5 0 0 1 5 0v2" stroke="currentColor" stroke-width="1.4" fill="none"/></svg>
Secured
</span>
</div>
<div class="ac-balance">
<span class="ac-balance-label">Available balance</span>
<strong class="amount" id="availBalance">$0.00</strong>
</div>
<div class="ac-stats">
<div>
<span class="ac-stat-label">Money in</span>
<strong class="amount credit" id="statIn">$0.00</strong>
</div>
<div>
<span class="ac-stat-label">Money out</span>
<strong class="amount debit" id="statOut">$0.00</strong>
</div>
<div>
<span class="ac-stat-label">Pending</span>
<strong class="amount" id="statPending">0</strong>
</div>
</div>
</section>
<section class="ledger" aria-label="Transaction history">
<div class="controls">
<div class="search">
<svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true"><circle cx="9" cy="9" r="6" stroke="currentColor" stroke-width="1.6" fill="none"/><path d="m14 14 3 3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
<input
type="search"
id="searchInput"
placeholder="Search merchant, note or amount…"
aria-label="Search transactions"
autocomplete="off"
/>
</div>
<div class="filters" role="group" aria-label="Filters">
<label class="select">
<span>Type</span>
<select id="typeFilter" aria-label="Filter by type">
<option value="all">All</option>
<option value="credit">Money in</option>
<option value="debit">Money out</option>
</select>
</label>
<label class="select">
<span>Category</span>
<select id="categoryFilter" aria-label="Filter by category">
<option value="all">All categories</option>
</select>
</label>
<label class="select">
<span>Range</span>
<select id="rangeFilter" aria-label="Filter by date range">
<option value="all">All time</option>
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
</label>
</div>
</div>
<div class="ledger-meta">
<span id="resultCount">0 transactions</span>
<button class="link-btn" type="button" id="clearBtn" hidden>Clear filters</button>
</div>
<div class="tx-list" id="txList" role="list"></div>
<div class="empty" id="emptyState" hidden>
<svg viewBox="0 0 48 48" width="40" height="40" aria-hidden="true"><circle cx="24" cy="24" r="22" stroke="currentColor" stroke-width="2" fill="none" opacity=".3"/><path d="M16 24h16M16 30h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<p>No transactions match your filters.</p>
<button class="ghost-btn" type="button" id="emptyClear">Reset filters</button>
</div>
</section>
</main>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Transactions
A complete banking transaction history built for trust and calm density. A navy gradient account card pins the masked card number, a secured badge and the available balance to the top, with money-in, money-out and pending counts summarised beneath it. Below sits the ledger: a searchable, filterable list grouped by day, each group showing a sticky date header and a net total so you can scan a week at a glance.
Every row pairs a category icon with the merchant, a status pill (pending, cleared or failed) and a right-aligned amount — credits in green, debits in ink — alongside the running account balance after that entry. All money uses tabular figures so columns line up precisely. Search matches merchant, note, category and amount; the type, category and date-range selects narrow the list instantly, and a clear-filters affordance appears whenever a filter is active.
Tap any transaction to expand an inline detail card with the reference, payment method, timestamp and per-row actions, or hit Export CSV to ship the current filtered view — both surfaced through a lightweight toast helper. The whole screen is keyboard-usable, AA-contrast and responsive down to about 360px, with no frameworks or build step.
Illustrative UI only — not real banking software or financial advice.