One temporal data type, three granularities — the method fixes which form a field stores.
The temporal family: one date data type holding an ISO-8601 string
in one of three forms, with the method fixing which form a field
stores. All three sort lexicographically in time order within their
form — the property everything else here leans on — and all three ride
native inputs, so the control is the platform's own.
| Method | Stores | Reach for it when |
|---|---|---|
| Date & Time | 2026-10-06T20:00 — a moment, local |
the hour is content: showtimes, openings |
| Date | 2026-10-06 — a day |
the day is the fact: deadlines, publish dates |
| Time | 20:00 — a time of no particular day |
recurring hours: opens-at, class times |
The forms are granularities, not formats: which one a field stores is part of its contract, because a template that prints or compares the value has to know what it holds. Mixing forms in one field would make sorting silently wrong, which is why the editor fixes the form rather than offering it per record.
And because the stored form sorts, "what's next" is one comparison —
rows.filter((r) => r.content.starts_at >= today) — no parsing, no
date library, string order IS time order.
Store the sortable form, print the human one:
<time datetime="<%= cms.asset.content.starts_at %>">
<%= new Date(cms.asset.content.starts_at).toLocaleString() %>
</time>
The stored value never carries a display format, a locale, or a timezone name — those are presentation decisions, and they belong at the print site where the audience is known.