Negative numbers in a financial table
Use parentheses instead of a minus sign, the real minus where a sign belongs, and a column that stays aligned when the sign changes. Almost nobody implements the third rule.
Which convention, where#
| Context | Format | Why |
|---|---|---|
| Statements, ledgers, tables | (1,234.00) |
The accounting standard. The reader is scanning a column, and parentheses stand out in peripheral vision where a leading hyphen doesn't. |
| Deltas, KPIs, inline prose | −$1,234.00 |
A delta is a change, not a balance. On its own, an explicit sign reads faster than a bracket. |
Both are colored --negative, and neither depends on color. The parens and the sign carry the meaning, so the interface survives a grayscale print and red-green color blindness. The showcase has a grayscale toggle in its header if you want to check.
Don't mix the two conventions in one table. A column where some negatives are bracketed and some are signed reads as two different measurements.
Use the real minus#
− is U+2212 MINUS SIGN. - is U+002D HYPHEN-MINUS, a different glyph: shorter, lower, and not the same width as +. In a column of deltas, +12.4% and -12.4% won't line up.
Intl won't save you. en-US and most locales emit U+002D, and only a few (Swedish, Finnish and others) emit U+2212. Swap the sign after formatting, or use currencySign: "accounting" and parentheses. The same bug shows up when someone hand-builds the string:
`-${format(Math.abs(value))}` // hyphen-minus. Wrong width, breaks the column.
The alignment problem#
This is the one teams skip. In a right-aligned column mixing 1,234.00 and (1,234.00), the closing paren pushes the digits about 4px left, and the decimal points stop lining up. It's subtle enough to pass review and obvious enough to look amateur next to a printed statement.
Three fixes, best first:
1. Reserve the paren column. Render an invisible ) on positive values.
<span aria-hidden className="invisible">)</span>
Cheapest, works everywhere, and keeps copy-paste clean if you strip aria-hidden content on copy.
2. Grid the cell. display: grid; grid-template-columns: auto 1fr auto; for open paren, figure, close paren. The sturdiest option for mixed content.
3. Pad the cell. padding-right: 1ch on positives. Only correct under font-variant-numeric: tabular-nums, where 1ch is a real fixed unit.
Treat the sign in a delta column the same way. Reserve the slot so a list of changes stays aligned even when some rows are unchanged.
Sign is not sentiment#
A negative number isn't bad news. A −4.2% change in burn is good; a −4.2% change in revenue isn't. Coloring by sign is how a dashboard shows cheerful green on a cost overrun.
So polarity is an explicit prop:
<Delta value={{ amount: 8200, currency: "USD" }} polarity="inverted" />
// ↑ +$8,200.00 — colored negative, because up is bad for this metric
standard | inverted | neutral, set per metric. There's no defensible default, so the component doesn't pick one for you.
Zero is not a small negative#
A true zero in a statement renders as an em dash (—), which means computed, and nil. A blank cell means not applicable here. n/a means we don't have this. None of them is $0.00. In transactional tables and inputs, where a figure is typed instead of read as a total, $0.00 is fine. See formatting money.
Where this is implemented#
Money:accountingandstatementmodes, and the reserved paren columnDelta: the polarity contract, and the reserved sign slotStatementRowandStatementTable: negatives and rule lines togetherVarianceCell: where a signed number and a polarity meet a budget- Statements, running: flip a value negative and watch the column hold
- Numerics: the full rule, with the three fixes ranked
- Color: why the sign never depends on hue alone