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:

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

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

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

  1. format
  2. canonical value plus dateFormat
  3. compatible numeric value plus numberFormat
  4. the channel’s existing fallback

Group display, group copy, and group CSV use the aggregate equivalent:

  1. formatAggregate
  2. canonical aggregate plus dateFormat
  3. compatible numeric aggregate plus numberFormat
  4. 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:

  1. Store or project date-only fields as YYYY-MM-DD | null at the application boundary.
  2. Replace Date, epoch, date-time, and localized filter operands with canonical strings.
  3. Use dateFormat for localized presentation rather than changing stored values.
  4. 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.

See also