Statements
No component library ships a <BalanceSheet />. That's the biggest gap in the market, and the reason Lopifi exists.
59. StatementTable#
The shared shell. The P&L, balance sheet and cash flow statement are all configurations of it.
Structure#
┌─ Toolbar: basis · period · comparison · currency · consolidation ─┐
├───────────────────────────────────────────────────────────────────┤
│ Account Sep 2026 Aug 2026 Change % │ ← sticky header
├───────────────────────────────────────────────────────────────────┤
│ Revenue │ section
│ Product revenue 482,100 441,200 +40,900 +9.3% │ L2
│ Subscriptions 390,400 360,100 +30,300 +8.4% │ L3
│ One-time 91,700 81,100 +10,600 +13.1% │ L3
│ Services 118,400 102,800 +15,600 +15.2% │ L2
│ ───────── ───────── ───────── │ ← single rule
│ Total revenue 600,500 544,000 +56,500 +10.4% │ subtotal
│ │
│ … │
│ ═════════ ═════════ ═════════ │ ← double rule
│ Net income 142,880 118,400 +24,480 +20.7% │ total
└───────────────────────────────────────────────────────────────────┘
The craft#
- Indentation: 24px per level (16px on phone). Levels 1–5. Anything deeper is a chart-of-accounts problem, not a UI one.
- Rule lines span the numeric columns only, each period column independently, gutters unruled. This is what makes it read as an accounting statement instead of a styled table.
- Sticky account column (left) and sticky header (top). Both, always. A P&L that loses its account names when you scroll sideways is unreadable.
- Sticky total row at the bottom for the grand total.
- Expand/collapse is instant, because it happens hundreds of times per close. The chevron rotates over 140ms; the rows don't slide, grow or stagger. This is the Tier 0 concession.
- Expansion state is in the URL, so a shared link opens to the same view.
- Zero values render as
—. Null renders asn/a. They are different facts. - Right-aligned,
tnum,case, paren column reserved. Negatives always in parentheses. - Every column header states its period explicitly. "Sep 2026", never "Current".
polarityis required on the table, no default. It's the root of inheritance: a line without its ownpolaritytakes its nearest ancestor's, and a top-level line takes the table's.ProfitAndLossandCashFlowStatementpassstandard(expense sections mark themselvesinverted);BalanceSheetpassesneutral. (API change: lines with no polarity anywhere above them used to fall back tostandard.)- The statement header carries the full context: entity, statement type, resolved period, basis (cash/accrual), presentation currency, scale, and generation timestamp. Print needs it, and the number means nothing without it.
- Scale is applied, not just labeled.
header.scaleofthousandsormillionsputs "in thousands" in the header and the caption and presents every figure, variance and hover change at that scale: divided from the exact minor units and rounded once, half away from zero, at display (presentScaledinlib/format-money). Per-currency decimals hold, so USD and JPY both scale to whole thousands of major units. The label and the figures read the same field, so they can't disagree. Rounded lines don't foot by construction; say so inroundingNote. Materiality is still tested on the exact amounts. - A line can cite its note.
note(a number) on aStatementLinerenders aNoteRefbeside the label, linking to that note inStatementNotes;noteTitlecompletes its accessible name, and the table'snoteIdPrefixmatches the notes'idPrefix.
Responsive#
One period column at a time on phone, with a swipeable period selector and a fixed account column. Values crossfade at 140ms with no translation, which would imply the labels moved too. Full spec in responsive.
Accessibility#
Real table semantics, <th scope="row"> on account names, <caption> with the resolved period, data-level for hierarchy (not aria-level, which is invalid outside a treegrid), aria-expanded/aria-controls on toggles, and subtotal/total status in the row header text, since a screen reader can't see the rule line. Full spec in accessibility.
Print#
Repeating headers, break-inside: avoid on rows, break-before: avoid on totals so a total never orphans from its rule.
60. StatementRow#
One line. Indent level, label, N period values, expand toggle, subtotal/total variant, drill-down affordance.
Server-safe (no "use client"), so a 400-row statement renders on the server. That's why Money, Rule and StatementRow are all RSC-compatible. It's a performance decision, not a purity one.
61. ProfitAndLoss#
Revenue → COGS → Gross profit → Operating expenses → Operating income → Other → Net income.
- Margin rows (gross %, operating %, net %) rendered as a
Percentlevel, with their period-over-period change inpoints, styled apart from the money rows so a percentage in a column of dollars doesn't look like dollars. - Comparison modes: prior period, prior year, budget, forecast. Each with its own variance column.
- Expense rows are
polarity="inverted". An increase in COGS isn't good news, and showing it green is exactly the bug this system exists to prevent. - Optional per-line
WaterfallChartdrill-in showing what drove the change.
62. BalanceSheet#
Assets → Liabilities → Equity, with the accounting identity checked and displayed.
- The balance check is a first-class UI element, not a footnote.
Assets 4,281,900 = Liabilities 1,840,200 + Equity 2,441,700 ✓. When it doesn't balance, it shows a prominent error with the difference, not a silently wrong statement. - Current vs. non-current sub-grouping.
- As-of date, not a period. A balance sheet is a point in time. The component takes a
Date, not a range, so it can't be labeled with one. - Comparative columns: this date vs. prior period end vs. prior year end.
63. CashFlowStatement#
Operating / Investing / Financing, with the reconciliation to the change in cash. Supports both direct and indirect methods. The beginning-cash → ending-cash tie-out is displayed and asserted, same as the balance sheet's identity.
- The direct method carries its reconciliation. With
method="direct",reconciliationtakes the lines that walk net income to net cash from operating activities (the indirect method's operating section) and the id of the direct statement's operating total. The lines render as a section below the net change in the same table, closed by a subtotal the component sums itself, and a check per period asserts the reconciliation reaches the direct method's operating cash, stating the difference when it doesn't. A direct-method statement without a reconciliation says so in an alert rather than rendering as complete. See statement semantics.
64. StatementToolbar#
Basis (cash / accrual) · period · comparison · presentation currency · consolidation scope · expand-all/collapse-all · export.
The basis toggle is prominent and always visible. A cash-basis P&L and an accrual-basis P&L are different statements, and a user who doesn't know which one they're reading is being misled. It doesn't belong in a settings menu.
65. PeriodPicker#
Fiscal-aware. Takes fiscalYearStartMonth and labels accordingly; FY26 Q1 may start in July.
- Presets: this month, last month, QTD, YTD, this fiscal quarter, this fiscal year, trailing 12.
- Every preset shows its resolved dates. "Last quarter (1 Apr – 30 Jun 2026)".
- Custom range with a two-month calendar.
- Closed/locked periods are visually marked and warn on selection, because a "closed" period's numbers shouldn't be changing.
66. VarianceCell#
Absolute change + percent change + significance. Significance is a threshold on both magnitude and materiality (a 200% swing on $12 isn't news), and the threshold is configurable because materiality is a business judgment.
polarity is required, no default (API change: it previously fell back to standard). Division by zero → n/a, never Infinity.
67. FluxAnalysis#
Period-over-period explanations attached to statement lines. Each variance can carry a written explanation with an author and timestamp. That's the real month-end close workflow, and no library supports it.
Unexplained material variances are flagged. The flag list is the reviewer's worklist.
68. DrilldownSheet#
Click a statement line → the GL entries behind it. Sheet on phone, side panel on desktop. The entries sum to the line, and the sum is shown, because that check is why you drill down. Links onward to source documents.
69. TrialBalance#
Every account with debit and credit columns and the proof that they're equal. The most boring component in the library, and the one an accountant checks first.
- The totals are the rows' sums.
totalDebitandtotalCreditare optional: omitted, each column is summed fromrows. Handed in (the totals the ledger reports), they're also checked against the rows' sums, and a total its own column doesn't reach is flagged with the sum it should have been.
70. CloseChecklist#
Tasks, owners, dependencies, blockers, sign-off. Progress by section. Overdue items flagged with day counts. Sign-off is an AlertDialog with hold-to-confirm, because signing off a close is irreversible.
183. ReportHeader#
The title block every statement and report carries: entity (or consolidated group), statement name, period or as-of date, basis, framework, presentation currency, scale, provenance, and the states that limit how far a reader may rely on it.
Northwind Holdings, Inc. and subsidiaries ← entity / consolidation scope
Consolidated profit and loss ← statement name
For the three months ended 30 Sep 2026 ← resolved period, or "As of" for a balance sheet
Accrual basis · US GAAP · Presented in USD · In thousands, except per-share amounts
Generated 2 Oct 2026, 09:12 UTC · Period closed · Xero
[Draft] [Unaudited] ← states, in words
- The period is a union, not a string.
{ start, end }resolves to "For the month ended…", "For the three months ended…", "For the year ended…";{ asOf }resolves to "As of…". A balance sheet labeled with a month can't be written. Anything other than whole calendar months states both ends instead of guessing a name;periodLabeloverrides for a 52/53-week year. - Scale is stated whenever it applies, with "except per-share amounts" when asked, because per-share figures are never scaled.
- States are words in a tint: Draft, Provisional, Restated, Unaudited, printed in that order (least reliable first). They survive a grayscale print. A restated header names its reason beside the label.
consolidatedprefixes the statement name; the entity line still names the group.- Server-safe,
forwardRef, extends<header>. One heading holds entity and statement, so the accessible name is what the document is.align="center"for the printed convention,size="compact"inside a pack. Three sizes, two weights. - Exports
reportPeriodLabelandreportScaleLabel, so an export or a filename says the same thing the screen does.
184. StatementOfChangesInEquity#
The fourth statement. Equity components across (common stock, additional paid-in capital, retained earnings, accumulated other comprehensive income, treasury stock, non-controlling interests, total), movements down (opening balance, net income, other comprehensive income, dividends, share issuances, buybacks, share-based compensation, closing balance).
Common APIC Retained AOCI Treasury Total
stock earnings stock equity
Balance as of 31 Aug 2026 1,200 138,800 81,000 1,600 — 222,600
Net income — — 82,880 — — 82,880
Foreign currency translation — — — 460 — 460
Dividends declared — — (66,580) — — (66,580)
… ─────── ─────── ───────── ─────── ───────── ────────
Balance as of 30 Sep 2026 1,215 152,065 97,300 2,060 (4,200) 248,440
═══════ ═══════ ═════════ ═══════ ═════════ ════════
- It foots both ways, and shows it. Each column walks opening plus movements and is proved against the closing balance the ledger reports (
closingis an input, never derived, so the check can fail). Each row sums across into the total column. The cross-foot proves the total column's walk equals the closing balances summed across. All of it renders as checks under the grid. - A column that misses gets an "Unexplained difference" row in the grid, in the column that misses, the way a roll-forward's difference owns the screen.
- Ties to the other statements when given:
tieOut.netIncomeasserts the net income row against the P&L, andtieOut.balanceSheetEquityasserts closing total equity against the balance sheet. - Non-controlling interests (
nonControlling: true) total after an "Attributable to parent" subtotal, and the net income row still ties to consolidated net income in total. - Sparse by nature, never zero by accident. An absent component in a movement is no movement and renders
—;nullis unknown, rendersn/a, and blocks every sum it touches. - Amounts are signed as they move equity: a buyback is negative in treasury stock, a dividend negative in retained earnings. Treasury stock sits in its natural position, parenthesized.
- The opening date is the day before the period starts, stated in the row. Movements and components take an optional
note, rendered as aNoteRef. ComposesReportHeader; server-safe.
185. StatementNotes#
Numbered notes, each with a title, a body and an optional small table, plus NoteRef, the marker on a statement line that links to its note.
- Every note is an anchor (
note-3, prefix configurable), reached withscroll-marginso a sticky header never covers it. NoteRefis a real link with a real name: visible "Note 3", accessible name "Note 3, Long-term debt". In print the underline goes and the number stays, because the number is how paper cross-references.- A note's table names its total; the component sums it. A note can't print a total its own lines don't reach. One
nullline makes the totaln/a, never a smaller total. - The table follows the statement conventions: rules over the numeric columns only, single above the total and double below, parentheses for negatives,
—for zero. - Print: a note's heading never strands at a page foot (
break-after: avoid), and its table never splits (break-inside: avoid). - Server-safe. Two sets of notes on one page take different
idPrefixvalues.
186. ReportingPackage#
A board or lender pack as one object: a cover (ReportHeader), a contents list of sections (statements, key figures, commentary, schedules, notes) with page numbers and anchors, prepared / reviewed / approved sign-offs with names, timestamps and versions, and one basis and period for the whole pack.
- The cover states the pack's basis, period and currency once. A section that departs from it (a cash-basis covenant schedule, a trailing-twelve-month KPI page) declares
basis,currencyorperiodLabel, and the contents flag the departure in words. A pack never mixes bases silently. - "Draft" is added to the cover until all three sign-offs stand on the version being read. No caller can print an unapproved pack without it.
- Approvals bind to a version. A sign-off recorded against an earlier version is shown as void, with both versions named.
- Segregation of duties is enforced, not configurable. A review or approval by the preparer is recorded, struck through, and doesn't count.
- No signatures are drawn. A sign-off is the recorded approval: name, title, timestamp with zone, version.
- Sections that carry
contentare linked from the contents and render below, each opening a printed page; sections without it list their page and link nowhere. - Server-safe. The contents are fragment links; nothing holds state.