Grid Date formatting
Date formatting
Strict calendar-date values with locale-aware native display.
Pretable treats type: "date" as a calendar date, not an instant. The built-in
value is exactly YYYY-MM-DD | null: ten ASCII characters for a valid
proleptic-Gregorian date in years 0000 through 9999, or null for empty.
It never guesses a time zone or accepts a JavaScript Date, epoch number,
date-time string, localized string, whitespace, partial date, or overflow such
as 2026-02-30.
Use an instant when the time and zone matter. Choose the application’s calendar zone and project a canonical date before a date-only value reaches the grid. Pretable deliberately does not make that domain decision for you.
Validate at the boundary
isValidDateValue is exported from both @pretable/core and
@pretable/react. Validate remote or untyped data before storing it as a
built-in date value:
import { isValidDateValue } from "@pretable/core";
export function calendarDateOrNull(value: unknown): string | null {
if (value === null) return null;
if (!isValidDateValue(value)) throw new Error("Expected YYYY-MM-DD or null");
return value;
}Validation does not normalize. A failed value remains application-owned data; the grid’s ordinary fallback can render it, but built-in date processing will not treat it as a date.
Rows mode
dateFormat opts a canonical value into native Intl.DateTimeFormat display.
The typed helper keeps the stored value, column type, filter operands, and date
aggregate outputs correlated:
import { createColumnHelper } from "@pretable/core";
import { PretableSurface } from "@pretable/react";
interface Invoice {
id: string;
due: string | null;
}
const column = createColumnHelper<Invoice>();
const columns = [
column.accessor("due", {
type: "date",
header: "Due",
aggregate: "max",
dateFormat: { dateStyle: "medium" },
}),
] as const;
export function InvoiceGrid({ rows }: { rows: readonly Invoice[] }) {
return (
<PretableSurface
ariaLabel="Invoices"
columns={columns}
locale="en-US"
rows={rows}
viewportHeight={360}
/>
);
}type: "date" selects strict editing, filtering, sorting, grouping, and
aggregation. It does not format by itself. Conversely, dateFormat is a
presentation option: on any column it formats only canonical string values and
otherwise leaves the normal fallback in control.
Explicit-model presentation
An explicit row model owns raw values and derivation. The typed column keeps its presentation contract when the model is supplied to a surface:
import { createColumnHelper, createLocalRowModel } from "@pretable/core";
import { PretableSurface } from "@pretable/react";
interface Schedule {
id: string;
startsOn: string | null;
}
const column = createColumnHelper<Schedule>();
const modelColumns = [
column.accessor("startsOn", {
type: "date",
aggregate: "min",
header: "Starts",
dateFormat: { year: "numeric", month: "short", day: "2-digit" },
}),
] as const;
const model = createLocalRowModel({
rows: [{ id: "s1", startsOn: "2026-08-18" }],
columns: modelColumns,
});
export const scheduleGrid = (
<PretableSurface
ariaLabel="Schedule"
columns={modelColumns}
locale="en-GB"
model={model}
viewportHeight={280}
/>
);Presentation-only model transitions, including dateFormat, do not invalidate
derived row work. With external filter or sort authority, Pretable still
publishes query state and formats the supplied rows locally; it does not
re-filter or reorder a server-controlled result.
Native option contract
PretableDateFormatOptions is a strict allowlist over native options:
localeMatcher, calendar, numberingSystem, dateStyle, weekday, era,
year, month, day, and formatMatcher. Time fields, timeStyle,
timeZone, and timeZoneName are forbidden. Unknown or symbol keys are also
rejected at runtime, including forbidden keys whose value is undefined.
Pretable internally anchors the canonical date at UTC before calling
Intl.DateTimeFormat; consumers cannot override that time zone. Native rules
still apply, including which granular fields may be combined with dateStyle
and how a non-Gregorian calendar represents low years.
The surface locale is forwarded unchanged. For server rendering, provide the
same explicit locale on server and client. UTC removes host-zone drift, but
identical text also requires compatible Intl/ICU locale data. If environments
differ, use a deterministic custom format callback or accept the normal
hydration constraint for localized output.
Sorting, filtering, and grouping
Without a custom comparator, canonical dates sort chronologically. Every
non-date value—including null, undefined, invalid strings, objects, and
numbers—forms one terminal rank and stays last in both ascending and descending
sorts. The date-specific terminal rank ignores the column’s nulls setting.
Equal dates and equal non-dates fall through to later sort keys and then stable
source order. Date-valued sibling groups use the same rule. A custom comparator
remains authoritative.
Built-in date filters compare canonical strings only. Invalid or empty cells
never match. In controlled or headless query state, a semantically invalid
string operand remains active and matches zero rows; a wrong JavaScript operand
type is a structured query error. A dateBetween filter also matches zero rows
when either string bound is invalid. In the React filter menu, incomplete,
cleared, or invalid input removes the menu-owned filter instead of preserving a
stale valid value.
Aggregates
Date columns support min, max, and count. min and max ignore empty or
noncanonical values and return a canonical string or null; count keeps its
existing numeric output. Date columns do not gain numeric sum or avg.
formatAggregate receives the correlated date-extremum or numeric-count output.
A date extremum may inherit dateFormat. A numeric count may inherit
numberFormat; it never inherits dateFormat.
One value-formatting precedence
Cells, copy, and CSV resolve a data value in the same order:
format- canonical value plus
dateFormat - compatible numeric value plus
numberFormat - the channel’s existing fallback
Group display, group copy, and group CSV use the aggregate equivalent:
formatAggregate- canonical aggregate plus
dateFormat - compatible numeric aggregate plus
numberFormat - the aggregate fallback
A custom cell renderer receives the resolved formattedValue but still owns
its rendered JSX. Export follows the value pipeline, not custom JSX. All
callbacks, row-model reads, transactions, editing, validation, and paste keep
the raw application value.
Formatted copy is intentionally not a paste round trip. A localized value such
as Aug 18, 2026 can be copied for people and CSV consumers, while the built-in
date paste parser trims user-entered text, then accepts only canonical
2026-08-18 or an empty result (which becomes null). Stored row values
remain strict and are never trimmed. Use custom copy/paste hooks when an application
needs a different symmetric interchange format.
Migration
This is a breaking correction for applications that previously supplied mixed date representations:
- Store or project date-only fields as
YYYY-MM-DD | nullat the application boundary. - Replace
Date, epoch, date-time, and localized filter operands with canonical strings. - Use
dateFormatfor localized presentation rather than changing stored values. - Use custom format/edit/filter hooks only when the application intentionally owns a different domain; display or edit hooks alone do not enable built-in date sorting, filtering, grouping, or aggregation.
Do not mechanically recommend date.toISOString().slice(0, 10): that silently
chooses the UTC calendar day and may differ from the domain’s intended local
day. If source data is an instant, first choose and document the calendar-zone
policy, then validate and project the resulting full-date string before it
reaches Pretable.