Banking — Transaction Row
A trust-first fintech transaction list built with vanilla HTML, CSS and JavaScript. Each row pairs a merchant avatar, name, category and date with a right-aligned amount that colors credits green and debits ink, plus a clear status pill for pending, cleared or failed states. Rows lift on hover and expand on click to reveal method, reference, settlement state and quick actions. Filter chips, live merchant search, masked card numbers and a small toast helper round out a polished, dependency-free pattern.
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.06);
--sh-3: 0 18px 48px rgba(14, 27, 58, 0.16);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1100px 460px at 100% -10%, #e9eefc 0%, rgba(245, 247, 251, 0) 60%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-variant-numeric: tabular-nums;
}
.app {
max-width: 720px;
margin: 0 auto;
padding: 28px 18px 64px;
}
/* ---------- Account card ---------- */
.account {
background: linear-gradient(160deg, var(--navy) 0%, var(--navy-2) 100%);
color: #fff;
border-radius: var(--r-lg);
padding: 20px 20px 16px;
box-shadow: var(--sh-3);
position: relative;
overflow: hidden;
}
.account::after {
content: "";
position: absolute;
inset: auto -40px -60px auto;
width: 220px;
height: 220px;
background: radial-gradient(circle, rgba(124, 92, 255, 0.45), transparent 70%);
pointer-events: none;
}
.account__head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.account__brand {
display: flex;
gap: 12px;
align-items: center;
}
.account__logo {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: 12px;
background: linear-gradient(135deg, var(--accent), var(--violet));
font-weight: 800;
font-size: 18px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
}
.account__name {
margin: 0;
font-weight: 700;
font-size: 15px;
}
.account__num {
margin: 2px 0 0;
font-size: 12.5px;
color: rgba(255, 255, 255, 0.72);
display: flex;
align-items: center;
gap: 6px;
}
.lock {
font-size: 11px;
}
.account__bal {
text-align: right;
}
.account__bal-label {
margin: 0;
font-size: 11.5px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.6);
}
.account__bal-amt {
margin: 2px 0 0;
font-weight: 800;
font-size: 22px;
}
/* ---------- Toolbar ---------- */
.toolbar {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-top: 18px;
position: relative;
z-index: 1;
}
.chip {
appearance: none;
border: 1px solid rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.85);
font: inherit;
font-size: 13px;
font-weight: 600;
padding: 7px 13px;
border-radius: 999px;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.08s ease;
}
.chip:hover {
background: rgba(255, 255, 255, 0.16);
}
.chip:active {
transform: translateY(1px);
}
.chip.is-active {
background: #fff;
color: var(--navy);
border-color: #fff;
}
.chip:focus-visible {
outline: 2px solid #fff;
outline-offset: 2px;
}
.toolbar__spacer {
flex: 1 1 auto;
}
.search {
display: flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 999px;
padding: 6px 12px;
color: rgba(255, 255, 255, 0.75);
}
.search input {
background: transparent;
border: 0;
outline: none;
font: inherit;
font-size: 13px;
color: #fff;
width: 130px;
}
.search input::placeholder {
color: rgba(255, 255, 255, 0.55);
}
/* ---------- Ledger ---------- */
.ledger {
margin-top: 20px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-2);
overflow: hidden;
}
.ledger__head {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 16px 18px 8px;
}
.ledger__title {
margin: 0;
font-size: 15px;
font-weight: 700;
}
.ledger__hint {
margin: 0;
font-size: 12.5px;
color: var(--muted);
}
.ledger__empty {
margin: 0;
padding: 32px 18px;
text-align: center;
color: var(--muted);
font-size: 14px;
}
.rows {
list-style: none;
margin: 0;
padding: 4px 0 6px;
}
/* ---------- Transaction row ---------- */
.row {
border-top: 1px solid var(--line);
}
.row:first-child {
border-top: 0;
}
.row__main {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 12px;
width: 100%;
border: 0;
background: transparent;
text-align: left;
font: inherit;
color: inherit;
padding: 12px 18px;
cursor: pointer;
transition: background 0.14s ease;
}
.row__main:hover {
background: var(--accent-50);
}
.row__main:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.row.is-open .row__main {
background: var(--accent-50);
}
.avatar {
display: grid;
place-items: center;
width: 42px;
height: 42px;
border-radius: 12px;
font-size: 19px;
background: var(--accent-50);
box-shadow: inset 0 0 0 1px var(--line);
}
.row__info {
min-width: 0;
}
.row__name {
margin: 0;
font-weight: 600;
font-size: 14.5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row__meta {
margin: 2px 0 0;
font-size: 12.5px;
color: var(--muted);
display: flex;
align-items: center;
gap: 7px;
}
.dot {
width: 3px;
height: 3px;
border-radius: 50%;
background: var(--line-2);
}
.row__right {
text-align: right;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 5px;
}
.amt {
font-weight: 700;
font-size: 14.5px;
color: var(--debit);
white-space: nowrap;
}
.amt.is-credit {
color: var(--credit);
}
.pill {
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 999px;
letter-spacing: 0.01em;
display: inline-flex;
align-items: center;
gap: 4px;
}
.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 panel ---------- */
.row__detail {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.26s ease;
}
.row.is-open .row__detail {
grid-template-rows: 1fr;
}
.row__detail-inner {
overflow: hidden;
}
.detail {
margin: 0 18px 14px;
padding: 14px 16px;
background: #fbfcfe;
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.detail__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 18px;
}
.detail__cell dt {
margin: 0;
font-size: 11px;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--muted);
}
.detail__cell dd {
margin: 2px 0 0;
font-size: 13.5px;
font-weight: 600;
color: var(--ink);
}
.detail__ref {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 12.5px;
font-weight: 500;
}
.detail__actions {
display: flex;
gap: 8px;
margin-top: 13px;
flex-wrap: wrap;
}
.btn {
appearance: none;
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink-2);
font: inherit;
font-size: 13px;
font-weight: 600;
padding: 7px 13px;
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.14s ease, transform 0.08s ease, border-color 0.14s ease;
}
.btn:hover {
background: var(--bg);
}
.btn:active {
transform: translateY(1px);
}
.btn--primary {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.btn--primary:hover {
background: var(--accent-d);
}
.btn--danger {
color: var(--danger);
border-color: rgba(212, 73, 62, 0.4);
}
.btn--danger:hover {
background: rgba(212, 73, 62, 0.08);
}
.btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.verified {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 600;
color: var(--ok);
margin-top: 11px;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%) translateY(16px);
background: var(--navy);
color: #fff;
font-size: 13px;
font-weight: 600;
padding: 11px 16px;
border-radius: 999px;
box-shadow: var(--sh-3);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 50;
}
.toast.is-show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.app {
padding: 18px 12px 56px;
}
.account__head {
flex-direction: column;
gap: 14px;
}
.account__bal {
text-align: left;
}
.search {
flex: 1 1 100%;
}
.search input {
width: 100%;
}
.toolbar__spacer {
display: none;
}
.detail__grid {
grid-template-columns: 1fr;
}
.row__main {
padding: 12px 14px;
gap: 10px;
}
.avatar {
width: 38px;
height: 38px;
font-size: 17px;
}
}
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;
}
}(function () {
"use strict";
/* ---------- Fictional but realistic data ---------- */
var TX = [
{ id: "TX-90412", name: "Bluebird Coffee Roasters", cat: "Food & Drink", icon: "☕", date: "Jun 16, 2026", time: "08:14", amount: -4.85, status: "pending", method: "Visa Debit •••• 4242", ref: "BLB-20260616-0814" },
{ id: "TX-90408", name: "Northvale Salary", cat: "Income", icon: "💼", date: "Jun 15, 2026", time: "00:01", amount: 3250.0, status: "cleared", method: "ACH credit", ref: "PAY-NV-2026-06" },
{ id: "TX-90405", name: "Helio Energy Utilities", cat: "Bills", icon: "💡", date: "Jun 14, 2026", time: "19:42", amount: -118.4, status: "cleared", method: "Direct debit", ref: "HEL-88231-06" },
{ id: "TX-90401", name: "Meridian Grocers", cat: "Groceries", icon: "🛒", date: "Jun 14, 2026", time: "17:09", amount: -76.23, status: "cleared", method: "Visa Debit •••• 4242", ref: "MER-44102-0917" },
{ id: "TX-90398", name: "Cascade Transit Pass", cat: "Transport", icon: "🚆", date: "Jun 13, 2026", time: "07:55", amount: -2.9, status: "pending", method: "Apple Pay", ref: "CTP-MONTHLY-06" },
{ id: "TX-90392", name: "Atlas Refund — Order #5521", cat: "Refund", icon: "↩️", date: "Jun 12, 2026", time: "13:20", amount: 42.99, status: "cleared", method: "Card refund", ref: "ATL-RF-5521" },
{ id: "TX-90387", name: "Pinecrest Gym", cat: "Health", icon: "🏋️", date: "Jun 11, 2026", time: "06:30", amount: -39.0, status: "failed", method: "Visa Debit •••• 4242", ref: "PIN-MEM-0611" },
{ id: "TX-90381", name: "Lumen Streaming", cat: "Subscription", icon: "🎬", date: "Jun 10, 2026", time: "00:05", amount: -12.99, status: "cleared", method: "Direct debit", ref: "LUM-SUB-0610" },
{ id: "TX-90376", name: "Harbor Books", cat: "Shopping", icon: "📚", date: "Jun 09, 2026", time: "15:48", amount: -27.5, status: "cleared", method: "Visa Debit •••• 4242", ref: "HRB-77120-1548" },
{ id: "TX-90370", name: "Transfer from J. Okafor", cat: "Transfer", icon: "👤", date: "Jun 08, 2026", time: "11:02", amount: 60.0, status: "cleared", method: "Instant transfer", ref: "TRF-OKAFOR-0608" }
];
var STATUS_LABEL = { pending: "Pending", cleared: "Cleared", failed: "Failed" };
var rowsEl = document.getElementById("rows");
var emptyEl = document.getElementById("empty");
var countEl = document.getElementById("count");
var searchEl = document.getElementById("search");
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip"));
var toastEl = document.getElementById("toast");
var activeFilter = "all";
var query = "";
var toastTimer;
function money(n) {
var sign = n < 0 ? "-" : "+";
var abs = Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
return sign + "$" + abs;
}
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2200);
}
function matches(tx) {
if (activeFilter === "pending" && tx.status !== "pending") return false;
if (activeFilter === "cleared" && tx.status !== "cleared") return false;
if (activeFilter === "credit" && tx.amount <= 0) return false;
if (query && tx.name.toLowerCase().indexOf(query) === -1 && tx.cat.toLowerCase().indexOf(query) === -1) return false;
return true;
}
function buildRow(tx) {
var li = document.createElement("li");
li.className = "row";
li.dataset.id = tx.id;
var credit = tx.amount > 0;
var panelId = "detail-" + tx.id;
li.innerHTML =
'<button class="row__main" type="button" aria-expanded="false" aria-controls="' + panelId + '">' +
'<span class="avatar" aria-hidden="true">' + tx.icon + "</span>" +
'<span class="row__info">' +
'<span class="row__name">' + tx.name + "</span>" +
'<span class="row__meta">' +
"<span>" + tx.cat + "</span>" +
'<span class="dot" aria-hidden="true"></span>' +
"<span>" + tx.date + "</span>" +
"</span>" +
"</span>" +
'<span class="row__right">' +
'<span class="amt' + (credit ? " is-credit" : "") + '">' + money(tx.amount) + "</span>" +
'<span class="pill pill--' + tx.status + '">' + STATUS_LABEL[tx.status] + "</span>" +
"</span>" +
"</button>" +
'<div class="row__detail">' +
'<div class="row__detail-inner">' +
'<div class="detail" id="' + panelId + '" role="region" aria-label="Transaction details">' +
'<dl class="detail__grid">' +
'<div class="detail__cell"><dt>Posted</dt><dd>' + tx.date + " · " + tx.time + "</dd></div>" +
'<div class="detail__cell"><dt>Category</dt><dd>' + tx.cat + "</dd></div>" +
'<div class="detail__cell"><dt>Method</dt><dd>' + tx.method + "</dd></div>" +
'<div class="detail__cell"><dt>Reference</dt><dd class="detail__ref">' + tx.ref + "</dd></div>" +
"</dl>" +
(tx.status === "cleared"
? '<p class="verified"><span aria-hidden="true">🔒</span> Verified & cleared</p>'
: tx.status === "pending"
? '<p class="verified" style="color:var(--warn)"><span aria-hidden="true">⏳</span> Awaiting settlement</p>'
: '<p class="verified" style="color:var(--danger)"><span aria-hidden="true">⚠️</span> Payment declined — retry available</p>') +
'<div class="detail__actions">' +
'<button class="btn btn--primary" data-act="receipt">View receipt</button>' +
'<button class="btn" data-act="copy">Copy reference</button>' +
(tx.status === "failed"
? '<button class="btn btn--primary" data-act="retry">Retry payment</button>'
: "") +
'<button class="btn btn--danger" data-act="dispute">Dispute</button>' +
"</div>" +
"</div>" +
"</div>" +
"</div>";
var trigger = li.querySelector(".row__main");
trigger.addEventListener("click", function () {
toggle(li, trigger);
});
li.querySelector(".detail__actions").addEventListener("click", function (e) {
var btn = e.target.closest("button[data-act]");
if (!btn) return;
var act = btn.dataset.act;
if (act === "copy") {
copyText(tx.ref);
toast("Reference " + tx.ref + " copied");
} else if (act === "receipt") {
toast("Receipt for " + tx.name + " opened");
} else if (act === "retry") {
toast("Retrying payment to " + tx.name + "…");
} else if (act === "dispute") {
toast("Dispute started for " + tx.id);
}
});
return li;
}
function toggle(li, trigger) {
var open = li.classList.toggle("is-open");
trigger.setAttribute("aria-expanded", open ? "true" : "false");
}
function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(function () {});
}
}
function render() {
var visible = TX.filter(matches);
rowsEl.innerHTML = "";
visible.forEach(function (tx) {
rowsEl.appendChild(buildRow(tx));
});
emptyEl.hidden = visible.length !== 0;
countEl.textContent = visible.length + (visible.length === 1 ? " item" : " items");
}
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
chips.forEach(function (c) {
c.classList.remove("is-active");
c.setAttribute("aria-selected", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-selected", "true");
activeFilter = chip.dataset.filter;
render();
});
});
searchEl.addEventListener("input", function () {
query = searchEl.value.trim().toLowerCase();
render();
});
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Banking — Transaction Row</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="app">
<section class="account" aria-label="Account summary">
<div class="account__head">
<div class="account__brand">
<span class="account__logo" aria-hidden="true">N</span>
<div>
<p class="account__name">Northvale Checking</p>
<p class="account__num">
<span class="lock" aria-hidden="true">🔒</span>
•••• 4242 · IBAN GB29 ···· 0918
</p>
</div>
</div>
<div class="account__bal">
<p class="account__bal-label">Available balance</p>
<p class="account__bal-amt" id="balance">$8,412.66</p>
</div>
</div>
<div class="toolbar" role="tablist" aria-label="Filter transactions">
<button class="chip is-active" role="tab" aria-selected="true" data-filter="all">All</button>
<button class="chip" role="tab" aria-selected="false" data-filter="pending">Pending</button>
<button class="chip" role="tab" aria-selected="false" data-filter="cleared">Cleared</button>
<button class="chip" role="tab" aria-selected="false" data-filter="credit">Income</button>
<div class="toolbar__spacer"></div>
<div class="search">
<span aria-hidden="true">⌕</span>
<input id="search" type="search" placeholder="Search merchant…" aria-label="Search transactions by merchant" />
</div>
</div>
</section>
<section class="ledger" aria-label="Transaction list">
<header class="ledger__head">
<h1 class="ledger__title">Transactions</h1>
<p class="ledger__hint" id="count">10 items</p>
</header>
<ul class="rows" id="rows"></ul>
<p class="ledger__empty" id="empty" hidden>No transactions match that filter.</p>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Transaction Row
A reusable banking transaction row, rendered as a calm-but-dense ledger under a navy account card. Every row leads with a rounded merchant avatar, then the merchant name with its category and posting date, and finishes with a right-aligned amount that uses tabular figures. Credits glow green while debits stay ink-dark, and a status pill marks each entry as pending, cleared or failed so the state is readable at a glance.
Rows respond to interaction: they tint and lift on hover, and clicking (or pressing Enter on the keyboard-focusable button) smoothly expands an inline detail panel. The panel surfaces the posting time, payment method with a masked card number, a monospaced reference, and a verification cue — a lock for cleared payments, an hourglass for pending settlement, or a warning with a retry option for declined ones. Quick actions let you view a receipt, copy the reference, retry, or open a dispute, each confirmed by a lightweight toast.
The toolbar adds filter chips (All, Pending, Cleared, Income) and a live merchant search that re-render the list instantly, with an empty state when nothing matches. Everything is self-contained vanilla JavaScript with realistic, clearly fictional merchants, IBANs and •••• 4242 card numbers, and the layout collapses gracefully down to roughly 360px for customer mobile screens.
Illustrative UI only — not real banking software or financial advice.