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:

Money, accounting, and percent formats

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.

.md

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:

tsx
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:

ts
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:

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