Skip to content

How to build a general ledger table in React

The data grid is the easy part. What makes a ledger table a ledger table is two amount columns, a visible balance check, and an entry you can reverse but never edit.


1. Debit and credit are two columns

This is the single most important rendering rule in the accounting family, and the one a generic data grid gets wrong by default.

type JournalLine = {
  accountId: string;
  /** Exactly one of these is non-zero. Never a single signed amount. */
  debit: Money;
  credit: Money;
  dimensions: Dimensions;
  memo?: string;
};

A single signed column — -1,240.00 — collapses two facts into one. Double-entry exists so that a person can verify the arithmetic by eye; collapse the columns and you have deleted the control while keeping the data. There is no single-column variant of a ledger table, and offering one as a "compact mode" is offering to remove the audit trail to save 90 pixels.

2. Show the balance check

Not "validate on save". Show it, at the foot of the entry, with the totals:

                              Debit        Credit
  6200 · Rent expense       4,500.00
  1010 · Operating cash                  4,500.00
                          ─────────    ─────────
                           4,500.00     4,500.00   ✓ balanced

An unbalanced entry is a prominent error state with the difference stated — never a silently saved draft, and never a discrepancy rounded away. When the difference is divisible by nine, a transposition or a slide is the likely cause, and saying so turns a ten-minute hunt into a five-second fix. That diagnosis is a tool of its own.

3. Show the normal balance

Five account types, each with a side that increases it:

Type Normal Statement Typical range
Asset Debit Balance sheet 1000–1999
Liability Credit Balance sheet 2000–2999
Equity Credit Balance sheet 3000–3999
Revenue Credit P&L 4000–4999
Expense Debit P&L 5000–8999

An account picker that shows Dr or Cr next to the account prevents a whole class of posting error for one glyph of ink. Contra accounts — accumulated depreciation, allowance for doubtful accounts — carry the opposite normal balance to their type and present as a deduction from their parent, so the picker has to state theirs rather than infer it from the type.

4. Posted entries are reversed, never edited

Immutability is not a database preference here, it is the product. A posted entry gets a reversing entry with its own number and date; the original stays exactly as it was. Which means the interface has three states to render, and the affordances differ:

  • draft — editable, deletable, not in any report
  • posted — immutable; the only actions are reverse and copy to new
  • reversed — immutable, and linked in both directions to the entry that reversed it

The period state gates all of it. A posted entry in a closed period cannot be reversed into that period, only into an open one — which is a different transaction with different consequences, and the UI has to say so before the click, not after.

5. The ledger and the statement disagree about signs

The same fact renders differently depending on which representation you are in. Revenue is a credit in the ledger and a positive number in the P&L. A component that does not know which representation it is rendering will get revenue backwards — and it will look plausible, which is worse than looking broken.

So the representation is a property of the component, not an inference from the value. The full argument is in the accounting model.

6. Virtualise, but keep the keyboard

A register with four thousand rows has to virtualise. It also has to stay one tab stop for the whole grid, with arrows to navigate, Space to select, Enter to open and Escape to clear — and correct aria-rowcount and aria-rowindex, which virtualisation breaks by default because the DOM no longer contains the rows the numbers refer to.

Sticky header, sticky total, no zebra striping. The tabular figures do the aligning.


Where this is implemented

Told when this spec changes

The docs are the contract the components are built against. When one changes, the component changes with it — and the copy in your repo does not. One email when that happens.

One address, no list rental, unsubscribe in one click.

Works in your QuickBooks Online or Xero file

Get your books closed every month

Lopifi is building AI bookkeeping that keeps your books closed on US GAAP as transactions happen, with a CPA review every quarter, for $200 a month. We’re not open yet.

$200/month when we open. No card now.