Transaction List
A finance-app transaction list React component: rows grouped by date automatically, rolling amounts coloured by sign, neighbouring rows shifting smoothly when a new transaction is inserted, plus swipe-to-pin and status chips.
The transaction list from the home screen of a banking or budgeting app. Pass in an array of transactions and the component groups them by local date (Today, Yesterday, otherwise a full date such as "Fri, Oct 23", with the year added across years), newest first within each group. Each row has a round icon — or a letter avatar built from the title — on the left, and on the right a rolling amount (income gets a "+" and an accent colour, spending stays neutral) with a Pending, Failed or Refunded chip. When a transaction is inserted its row expands in and the rows below shift down smoothly; swipe any row to the right or press its pin to move it into the Pinned group at the top, where it flies over with a layout animation. Pinning supports controlled and uncontrolled modes.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add transaction-list.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<TransactionList/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/transaction-list.en.jsonOr, once registries are configured in components.json, install it as @webberui/transaction-list.
Usage
import { TransactionList } from "@/components/ui/transaction-list";
<TransactionList
items={[
{ id: "1", date: "2026-10-23T09:00:00", title: "Dawnlight Design Ltd.", subtitle: "Salary", amount: 52000 },
{ id: "2", date: "2026-10-23T14:20:00", title: "Corner Mart", subtitle: "Groceries", amount: -85, status: "pending" },
{ id: "3", date: "2026-10-22T19:30:00", title: "Cloudlet Video", subtitle: "Monthly subscription", amount: -390, status: "failed" },
]}
currency="TWD"
defaultPinnedIds={["1"]}
onSelect={(id) => console.log(`Selected ${id}`)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | TransactionItem[] | — | Transactions in any order; the component sorts them newest first |
grouped | boolean | true | Group rows by date (Today / Yesterday / full date); when off, a single list with the date in each subtitle |
currency | string | "TWD" | Default currency (ISO 4217), used when an item has no currency |
pinnedIds | string[] | — | Controlled list of pinned ids; managed internally when omitted (uncontrolled) |
defaultPinnedIds | string[] | [] | Initial pinned ids in uncontrolled mode |
onPinnedChange | (ids: string[]) => void | — | Called when pinning changes, with the new list of pinned ids |
onSelect | (id: string) => void | — | Called when a row is selected; when provided, each row becomes a focusable button |
stiffness | number | 380 | Spring stiffness for row shifts, expansion and rolling amounts |
emptyState | ReactNode | Receipt icon + "No transactions yet" | Content shown when there are no transactions |
now | Date | — | Reference time for Today / Yesterday; when omitted, new Date() is used after mount and the first render shows full dates |
className | string | — | Passed through to the outer container |
TransactionItem
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique id; also deterministically picks the letter avatar's tone |
date | string | Date | — | Transaction time (ISO string or Date); rows are grouped by local date |
title | string | — | Title (merchant or counterparty) |
subtitle | string | — | Subtitle (category, note…), shown alongside the HH:mm time |
amount | number | — | Amount; positive is income, negative is spending |
currency | string | the component's currency | ISO 4217 currency code |
status | "completed" | "pending" | "failed" | "refunded" | "completed" | Transaction status; completed shows no chip |
icon | ReactNode | — | 40px round icon on the left; without it a letter avatar is built from the title's first character |
How it works
- Amounts are formatted with
Intl.NumberFormat— no decimals for TWD, up to two for other currencies. The Chinese build uses the zh-TW locale, which renders TWD as a bare "$", so the component swaps in "NT$" to avoid confusion with US dollars. - Rolling digits: each digit is a 0–9 column that springs to its value (
stiffness, damping 20); non-digit characters such asNT$and commas stay static. When the amount or currency changes, digits roll from the old value to the new one. - Inserts and removals: a new row expands from zero height while the rows and date groups below shift down; a removed row collapses in reverse. Pinning and unpinning fly the row from its old spot to the new group via a shared layoutId, without the height expansion.
- Swipe to pin: rows drag horizontally, revealing a Pin hint on the left; releasing past the threshold toggles the pin, and the release after a drag never fires
onSelect. - Colour variables: income colour
--wb-txl-positive(dark mode--wb-txl-positive-dark); status chips--wb-txl-pending,--wb-txl-failedand--wb-txl-refunded, each with-bgand-darkvariants. - SSR: without
now, the first render shows full dates with the year for every group title and switches to Today / Yesterday after mount, so server and browser clocks never cause a hydration mismatch.
Accessibility
- Each date group is a
sectionlabelled by its heading viaaria-labelledby; rows areul/li - With
onSelect, each row is a button whosearia-labelreads the title, income or expense amount, date and time, and status — for example "Corner Mart, Expense NT$85, Today 14:20, Pending" - Rolling digits are hidden from assistive tech, with a screen-reader-only full amount next to them
- The pin button exposes its state with
aria-pressedand is reachable with Tab and toggled with Enter / Space; swiping is only an extra shortcut - Inserted transactions are announced through
aria-live="polite" - When the user has Reduce Motion enabled: no entrance stagger, amounts appear without rolling, inserted rows appear without the height animation, and pinning swaps rows instantly
Receipt Printer
A receipt that prints after checkout like a thermal printer feeding paper line by line: processing, printing and complete stages, a serrated paper edge, tear-off and replay.
Payment Success Sheet
A full bottom-sheet payment flow React component: the sheet slides up, the pay button morphs into a processing ring, the ring morphs into a check with a single pulse, and the order summary unfolds row by row.