Skip to content
OGE logoOGE

Dates and time zones

A birthday that shows a day early in New York is the most common date bug in business apps. It happens when a calendar day travels as a moment in time and the browser re-reads that moment on its own clock. OGE keeps the two apart: values stay instants, and a timeZone input says which clock shows and edits them.

timeZone date-only values no one-day shift

Two kinds of dates

  • Instants — when an order was placed, when a meeting starts. The same moment everywhere; only the clock that displays it differs.
  • Calendar dates — a birthday, a due date, a holiday. The same day everywhere; there is no moment to convert.

Instants on a chosen clock

Without timeZone an editor shows the browser's clock. Set it to show another one — the office's, the customer's — wherever the user is. Picking 09:00 in a timeZone="Europe/Istanbul" box stores 09:00 Istanbul time, whatever the browser's zone; the value bound to [(value)] is a plain Date for that instant.

<!-- an instant: the moment an order was placed, a meeting starts -->
<!-- shown on the office's clocks wherever the browser is -->
<oge-date-box type="datetime" timeZone="Europe/Istanbul" [(value)]="meetingAt" />

<oge-grid [data]="orders" keyField="id" timeZone="Europe/Istanbul">
  <oge-column field="placedAt" dataType="datetime" />
</oge-grid>

Date-only values

Pick one representation and keep it end to end. If the server stores calendar dates as UTC midnight, read and write them on UTC's clocks:

<!-- a date-only value the server keeps as UTC midnight (2026-03-05T00:00:00Z) -->
<!-- read and write it on UTC's clocks, so no browser zone shifts the day -->
<oge-date-box timeZone="UTC" [(value)]="birthday" />

<oge-grid [data]="people" keyField="id">
  <oge-column field="birthday" dataType="date" timeZone="UTC" />
</oge-grid>

If they travel as yyyy-MM-dd strings, parse them as local calendar days with toLocalDate() and write them back with serializeLikeOriginal(); the grid's date columns and editors already do both for string cells.

import { serializeLikeOriginal, toLocalDate } from '@oge-ui/core';

// a date-only value kept as 'yyyy-MM-dd': parse it as a LOCAL calendar day —
// never with new Date('2026-03-05'), which reads it as UTC midnight
const due = toLocalDate(row.due); // Thu Mar 05 2026 00:00 local

// write it back in the same shape after an edit
row.due = serializeLikeOriginal(picked, row.due); // '2026-03-20'

What timeZone covers

Surfaces that follow timeZone
ComponentOn the zone’s clock
oge-date-box · <OgeDateBox> the field, the calendar, Today / Now, typed and picked values, min / max
oge-calendar · <OgeCalendar>value , values , range , today, min / max
oge-grid · <OgeGrid> ( timeZone on the grid or a column) cell text, value lists and the date tree, the filter row, quick dates, condition filters, cell editors, CSV / PDF text and typed Excel dates
oge-scheduler · oge-gantt their own timeZone input (already zone-aware)
Grid group intervals (by day, month, year) not covered: the data source groups — in memory, on the runtime zone; group on a server for another zone

Server filters

A day filter on a zoned column becomes the half-open range between two instants — that zone's midnights, 23 or 25 hours apart on a DST day — so the server compares instants and needs no zone logic. The filter contract has the full rules.

// the grid sends instants: a day filter on a column with timeZone="Europe/Istanbul"
{ type: 'and', operands: [
  { type: 'binary', field: 'placedAt', op: 'ge', value: '2026-03-04T21:00:00.000Z' },
  { type: 'binary', field: 'placedAt', op: 'lt', value: '2026-03-05T21:00:00.000Z' },
] }
// compare them as instants (timestamptz / DateTimeOffset) — no zone math on the server