Grid Number formatting
Number formatting
Locale-aware decimal, money, and accounting display with native Intl options.
Pretable formats numeric cells with the platform's native Intl.NumberFormat:
put numberFormat on a column and pass the app's locale to the surface. The
regional sales grid below runs three of them side by side — money on Revenue,
accounting on Refunds, and a raw percent on Margin:
A regional sales grid using numberFormats.money, numberFormats.accounting, and a raw percent numberFormat, grouped by region so each aggregate row inherits its column's format with no formatAggregate callback.
locale is a prop on <Pretable>, <PretableSurface>, and
<LabeledGridSurface> alike; the grid above passes "en-US". The formatter it
compiles for a column is a single instance, and the same one serves the data
cells, the inherited aggregates on those region rows, and the built-in
clipboard serializer.
Native options, not format codes
numberFormat accepts Intl.NumberFormatOptions, so the grid uses the same
option names and locale data as the rest of your app — there is no separate
format-code DSL to learn or translate. The grid above declares it exactly this
way:
import { numberFormats, type PretableColumn } from "@pretable/react";
const columns: PretableColumn<Order>[] = [
{
id: "revenue",
header: "Revenue",
type: "number",
aggregate: "sum",
numberFormat: numberFormats.money({ currency: "USD" }),
},
{
id: "refunds",
header: "Refunds",
type: "number",
aggregate: "sum",
numberFormat: numberFormats.accounting({ currency: "USD" }),
},
];Keep the options object stable when possible. Pretable compiles one native
formatter per configured column and reuses it until that options object or the
surface locale changes.
numberFormat is a column option on the typed
createColumnHelper path too, so the
same presentation is available without hand-declaring PretableColumn:
import { createColumnHelper, numberFormats } from "@pretable/core";
const column = createColumnHelper<Order>();
const columns = [
column.accessor("revenue", {
type: "number",
header: "Revenue",
aggregate: "sum",
numberFormat: numberFormats.money({ currency: "USD" }),
}),
] as const;Decimal formatting is opt-in
type: "number" alone never formats a value. It selects numeric filtering,
editing, clipboard hints, and the default end alignment, but display remains
the raw value until you add numberFormat or format. The Margin column in
the grid above is a raw Intl.NumberFormatOptions object rather than one of
the currency presets — no preset is required to opt in:
const columns: PretableColumn<Order>[] = [
{
id: "marginPct",
header: "Margin",
type: "number",
aggregate: "avg",
numberFormat: {
style: "percent",
maximumFractionDigits: 1,
},
},
];Pretable does not force two decimal places. Fraction digits, significant
digits, grouping, notation, sign display, units, and percentages all follow
the native options you provide and the locale's Intl defaults.
Money and accounting presets
numberFormats.money({ currency }) and
numberFormats.accounting({ currency }) produce strict native
Intl.NumberFormatOptions. Both force style: "currency"; money forces
currencySign: "standard", and accounting forces
currencySign: "accounting". The exported
PretableCurrencyFormatOptions type requires currency and excludes
consumer-supplied style and currencySign while leaving the rest of the
native options available.
The helpers do not impose a two-decimal policy. Native currency rules still
decide the defaults unless you explicitly provide fraction-digit options.
They are small option builders, not formatter instances, and both
numberFormats and PretableCurrencyFormatOptions are exported from
@pretable/core and @pretable/react.
Locale and server rendering
Treat locale as app presentation context: pass the same value your dates,
messages, and other localized UI use. It is forwarded unchanged to
Intl.NumberFormat and also reaches the public SerializeRangesArgs passed to
onCopy. Calling serializeRanges directly accepts the same optional
locale.
If locale is omitted, the JavaScript runtime's default locale is used. An
explicit locale is required for deterministic server-rendered hydration;
otherwise the server and browser can choose different defaults and produce
different initial text. Keep the explicit locale identical across the server
render and hydration.
Aggregates inherit number formatting
A numeric group aggregate inherits its column's numberFormat for final
display and built-in clipboard output. In the grid above, grouping by region
folds Revenue, Refunds, and Margin into per-region aggregate rows — none of
the three declares a formatAggregate callback, yet each aggregate reads in
the same money, accounting, or percent presentation as its data cells,
because inheriting numberFormat is what happens by default.
formatAggregate still has the highest aggregate precedence. When present, it
wins over the inherited numberFormat; the ordinary data-cell format
callback is never called for a group aggregate because a group row has no data
row to pass it.
Display, clipboard, and raw-value boundaries
The built-in display and clipboard channels agree whenever a column supplies a
format callback or numberFormat configuration. Group rows likewise use
formatAggregate first and inherited numberFormat second in both channels.
Without either configuration, the fallback remains channel-specific. Display uses the renderer's ordinary text coercion, while clipboard serialization keeps its existing TSV/HTML-safe coercion for dates, objects, and other values.
Formatting never changes the row model. Sorting, filtering, grouping, aggregation, editing, validation, paste, and transactions continue to receive raw values and never receive formatted strings.
Custom values and callbacks
Native number formatting accepts JavaScript number and bigint values.
Numeric strings and Decimal-like objects are deliberately not coerced; use a
column format callback when those are your domain values.
For data cells, format outranks numberFormat. A column render callback
then receives the winning string as formattedValue, so custom JSX does not
need to repeat the formatting work. On <LabeledGridSurface>, column.render
wins first; otherwise its label/value wrapper runs, and the wrapper's
formatValue receives both raw value and the already resolved
formattedValue.
For group aggregates, formatAggregate outranks inherited numberFormat.
These two callback rules let domain-specific values or aggregate labels replace
native formatting without changing any raw-data behavior.
Deliberate limits
Native number formatting is intentionally a presentation primitive. Pretable
does not add dash-for-zero behavior, numeric alignment beyond the existing
column align option, an export subsystem, or a spreadsheet-style format-code
DSL. Use format or formatAggregate for domain-specific text, CSS or align
for layout, and the clipboard hooks or your own export pipeline for other
channels.