More Dashboards Snippets
Transaction List — Free HTML CSS JS Snippet
Transaction List · Dashboards · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Transaction List — Banking-App Transaction History with Day Grouping and Filters

The grouped transaction list is the core screen of every banking and fintech app — Revolut, Monzo, Cash App, and every neobank render payments the same way: rows with a category icon, merchant name, and signed amount, bucketed under sticky date headers with a per-day total. This component builds that exact pattern in HTML, CSS, and vanilla JavaScript, with income/spending filter tabs, correct currency formatting via Intl.NumberFormat, and staggered entry animations.
Grouping rows into day buckets
The data is a flat TXNS array; render() folds it into day buckets in a single pass. It walks the filtered rows in order and either appends to the current bucket or starts a new one when the day changes, accumulating a running net total per bucket as it goes. Because the grouping happens at render time rather than in the data, the same array can be re-filtered (all / income / spending) and regrouped instantly — the day headers and their net totals always reflect exactly the rows on screen, so switching to "Spending" shows each day's total outflow rather than the mixed net.
Sticky date headers
Each day header uses position: sticky; top: 0 inside the scrollable list, so as you scroll, the current day's label pins to the top and is pushed away by the next one — the signature interaction of mobile banking feeds. The header carries the day's net total right-aligned, formatted with a leading + when positive. Sticky positioning needs an opaque background (#fff here) or rows would show through while pinned.
Real currency formatting
Amounts never go through manual string concatenation. new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }) handles the dollar sign, thousands separators, two-decimal padding, and — crucially — negative formatting, so -84.12 renders as -$84.12 without sign-juggling code. Change two constructor arguments to localise the whole list to EUR, GBP, or INR. The amounts also set font-variant-numeric: tabular-nums so digits align vertically down the column, which matters as soon as two rows sit next to each other.
Category icons without an icon font
Each transaction names an icon key, a pastel tint for the 40px rounded square, and a stroke colour, and the ICONS map holds six inline SVGs (bank, coffee, cart, refund, car, subscription). Inline SVG keeps the snippet dependency-free and lets the stroke colour be injected per transaction, so income rows read green and each spending category gets its own hue — the visual scanning aid that makes these lists parseable at a glance.
Filter tabs and the empty state
The pill tabs write a filter key (all, in, out) and re-render; filtering is a one-line predicate on the sign of amount. If a filter produces no rows the list renders an explicit empty-state message instead of a blank area — a small detail that separates production UI from demos. Rows animate in with a 40ms staggered translateY fade (the same cascade technique as the animated list), so every filter switch feels responsive rather than jarring.
Customisation
Feed TXNS from your API (map day from a date formatter like Intl.DateTimeFormat with relative labels for today/yesterday), extend ICONS with your categories, and adjust the max-height to fit your layout. Clicking a row is already wired as a pointer target — attach a handler to open a detail sheet or a bottom sheet with the full transaction.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Ask an AI coding assistant like Claude to explain exactly how render()'s single-pass fold buckets a flat, sorted transaction array into day groups with running net totals — and why doing that grouping at render time (rather than pre-grouping the data) is what lets the same array serve all three filter tabs correctly. It's also worth a design-choice check: ask why the sticky date headers need an opaque background to work correctly, and what visual bug you'd see without one. For extending it, ask for a search box that filters by merchant name across all days, a monthly summary total pinned above the list, or swapping the six-icon lookup for a richer per-merchant logo system with a fallback initial-letter icon. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a grouped transaction history list in plain HTML, CSS, and JavaScript matching the banking-app pattern — day-grouped rows with sticky headers, filter tabs, and correct currency formatting — no framework, no charting library.
Requirements:
- Store transactions as a flat array (each with a day label, name, category, signed amount, and an icon key), sorted newest-first, and compute day groupings at render time by walking the array once: start a new bucket whenever the day label changes from the previous row, and accumulate a running net total (sum of signed amounts) within each bucket.
- Render each day bucket as a sticky-positioned header (position: sticky, top: 0, with an opaque background matching the card) showing the day label and its computed net total with a leading plus sign when positive, followed by that day's transaction rows.
- Format every amount using Intl.NumberFormat with a currency style — never manually concatenate a currency symbol or hand-roll thousands separators or negative-sign formatting — and apply tabular-nums to keep amount digits aligned down the column.
- Represent each transaction's category icon as inline SVG (no icon font, no image files) with a per-transaction tinted background circle and a stroke color injected per row, so income rows read in one color family and each spending category gets a distinct hue.
- Implement pill-style filter tabs (All / Income / Spending) that re-run the same grouping fold against a filtered subset of the array, so switching tabs recomputes both the visible rows and each day's displayed net total to match only what's shown.
- If a filter produces zero matching rows, render an explicit empty-state message rather than leaving a blank list, and animate rows into view with a staggered fade/rise using an increasing per-row animation delay on every re-render.Step by step
How to Use
- 1Paste the HTML, CSS, and JSA card renders with eight transactions grouped under Today, Yesterday, and Jun 30, each day header showing its net total on the right.
- 2Scroll the listDate headers stick to the top of the scroll area and hand off to the next day's header — the classic banking-feed interaction.
- 3Switch filter tabsClick Income or Spending — the list regroups instantly, day totals recompute for the visible rows, and rows cascade back in with a stagger.
- 4Check the empty stateFilters that match nothing show a "No transactions" message instead of a blank panel.
- 5Swap in your dataReplace the TXNS array with API data — name, category, time, signed amount, and an icon key per row; add categories to the ICONS map.
- 6Localise the currencyChange the Intl.NumberFormat locale and currency arguments to render EUR, GBP, INR, or any other currency correctly.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
render() walks the filtered array in order and starts a new bucket whenever the day field changes, pushing rows into the current bucket and adding each amount to that bucket's net. Grouping at render time (instead of storing pre-grouped data) means the same flat array serves every filter — switch to Spending and the buckets and totals are rebuilt for just the negative rows in one pass.
A sticky element stays in the document flow, so rows scroll underneath it while it is pinned. Without an opaque background the pinned header would visually overlap the row text sliding beneath it. Setting background: #fff (matching the card) plus a z-index makes the hand-off clean — the next day's header pushes the previous one out of view, which is the behaviour users know from banking apps.
Map your API timestamps through a labeller before rendering: compare each date to the current day and emit "Today"/"Yesterday" for the last 48 hours, otherwise format with Intl.DateTimeFormat (for example { month: 'short', day: 'numeric' } gives "Jun 30"). Because grouping only compares consecutive day strings, any labelling scheme works as long as rows arrive sorted newest-first.
Edit the one Intl.NumberFormat line: new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }) renders 1.234,56 € with German separators; 'en-IN' with INR gives ₹ and lakh/crore grouping. Every amount in rows and day headers flows through this single formatter, so there is exactly one place to localise — never concatenate currency symbols manually.
Keep the transactions array and active filter in state, compute the day buckets with the same fold inside a useMemo / computed / getter, and render nested maps (days → rows) instead of innerHTML strings. The filter tabs become buttons that set state. Sticky headers, tabular-nums, and the stagger animation are pure CSS and port unchanged; set each row's animationDelay inline from its index.