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.
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
| Component | On 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