Commerce & Checkout

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.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

380
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
itemsTransactionItem[]—Transactions in any order; the component sorts them newest first
groupedbooleantrueGroup rows by date (Today / Yesterday / full date); when off, a single list with the date in each subtitle
currencystring"TWD"Default currency (ISO 4217), used when an item has no currency
pinnedIdsstring[]—Controlled list of pinned ids; managed internally when omitted (uncontrolled)
defaultPinnedIdsstring[][]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
stiffnessnumber380Spring stiffness for row shifts, expansion and rolling amounts
emptyStateReactNodeReceipt icon + "No transactions yet"Content shown when there are no transactions
nowDate—Reference time for Today / Yesterday; when omitted, new Date() is used after mount and the first render shows full dates
classNamestring—Passed through to the outer container

TransactionItem

FieldTypeDefaultDescription
idstring—Unique id; also deterministically picks the letter avatar's tone
datestring | Date—Transaction time (ISO string or Date); rows are grouped by local date
titlestring—Title (merchant or counterparty)
subtitlestring—Subtitle (category, note…), shown alongside the HH:mm time
amountnumber—Amount; positive is income, negative is spending
currencystringthe component's currencyISO 4217 currency code
status"completed" | "pending" | "failed" | "refunded""completed"Transaction status; completed shows no chip
iconReactNode—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 as NT$ 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-failed and --wb-txl-refunded, each with -bg and -dark variants.
  • 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 section labelled by its heading via aria-labelledby; rows are ul / li
  • With onSelect, each row is a button whose aria-label reads 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-pressed and 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