ReferenceComponent utilities tokens
Someone can meet the same event half a dozen times. An event page, a checkout summary, a listing card, a report. They should see the date and time spelled the same way every time.
In React, always a component. The styles below are how you configure one, not things you call yourself.
| What you are showing | Component | Configured with | Reads | Without React |
|---|---|---|---|---|
| A moment | <DateTime> | dateStyle, timeStyle | Fri 27 Nov 2026, 7:30pm | formatLong |
| A moment, as elapsed time | <DateTime relative> | cutoffMs | 3 minutes ago | formatRelative |
| A span between two moments | <DateTime to> | showDuration, sameNight | Fri 27 to Sun 29 Nov 2026 | formatDateRange |
| A length of time | <Duration> | durationStyle | 2 hours 30 minutes | formatDuration |
| Time remaining, ticking | <Countdown> | seconds, display | 4:32 | formatDuration |
| A calendar tile | <CalendarTile> | weekday, dateTime | NOV 27 | formatGlyph |
| A chart axis tick | <DateTime render> | dateStyle 'short', timeStyle 'numeric' | 27 Nov 19:30 | formatDateTime |
A component renders a time element and sets its machine-readable value for you. That attribute is easy to get wrong, and wrong silently.
Every component is a thin call into a formatter, and the formatters are exported. The test: if it renders, use the component; if it is a string going into an attribute, a file or another system, use the formatter. Each component page lists its own under Without React.
Two cases look like exceptions and are not. A chart tooltip is ordinary HTML, so DateTime works there; only the axis is SVG, and render covers it. A range has no element of its own, which argues against a single time, not against a component: <DateTime to> renders one per end.
One scale, from spelled out to machine readable. Each step drops exactly one thing. Picking a style is a question about the reader. Do they need the weekday? Do they need the year?
| Style | Renders | Reach for it when |
|---|---|---|
| full | Friday, 27 November 2026 | A single date with room around it. The date is the point of the page. |
| long | Fri 27 Nov 2026 | The everyday default. Lists, cards, table rows, summaries. |
| medium | 27 Nov 2026 | The day of week does not help the reader. Order dates, settlement dates. |
| short | 27 Nov | Context already supplies the year. Chart ticks, group headings. |
| iso | 2026-11-27 | Exports, filenames, anything sorted or parsed. Never shown to a customer. |
The names are Intl.DateTimeFormat’s. The output departs from it twice, on purpose. long keeps a weekday where Intl drops it. iso replaces Intl’s numeric short, which renders 27/11/26 and is ambiguous outside Australia.
Time is a separate option. It is not a property of the date style. Any date style can carry a time. A date style on its own never does. The step names are the same words, so medium means the same kind of thing on both axes: the everyday form, one step down from the fullest.
| Style | Renders | Reach for it when |
|---|---|---|
| long | 7:30pm AEDT | The reader may be elsewhere and has to act on it. On-sale times, national tours, anything someone sets an alarm for. |
| medium | 7:30pm | The default. The reader is in the venue’s zone, or the zone is already established nearby. |
| short | 7:30pm · 7pm | A dense row where the time is one fact among several. A ticket tag, a chip, a chart tooltip. |
| numeric | 19:30 | Charts and dense tables. Fixed width, sorts as it reads. A buyer should never meet it. |
| (omitted) | not shown | The date alone is the answer. A festival’s date range, a settlement date. |
A date and a time are joined with a comma, which is the platform’s own separator. The meridiem closes up against the digits at every style, which is where the house style departs from the platform. Under iso the time switches to 24 hour, joins with a space and carries no zone name, because a spreadsheet cell is read as local wall-clock. It carries no offset either, which is why formatMachine exists for markup.
The choice is about the reader, not about space. long and medium differ by whether the reader can be trusted to know the zone. short is the one place minutes are dropped, so reach for it only where the time is incidental.
long means different things on the two axes, exactly as it does in Intl. dateStyle: 'long' is Fri 27 Nov 2026; timeStyle: 'long' adds the zone. They are independent, so { dateStyle: 'medium', timeStyle: 'long' } is an ordinary pairing.
A sale opening is the clearest case. Somebody in Perth needs to know whether to set an alarm for 9am or 6am.
On sale Fri 27 Nov, 9:00am AEDTDoors 7:30pmSold 7:30pm · Sec A Row 12
Do
On sale Fri 27 Nov, 9:00amDoors 7:30pm AEDT AEDTDoors 7pm
Don’t
short may do.Pick by what the reader needs, not by which screen you are on. What is not allowed is two spellings of the same thing in different places.
Abbreviated with abbreviated, full with full. A date never mixes registers with itself. The scale guarantees this, including the months the locale spells with four letters, such as Sept. You only break it by hand rolling.
Fri 27 NovFriday, 27 November
Do
Fri, 27 NovemberFriday, 27 Nov
Don’t
Show it unless something already on screen establishes it. Measured in the venue’s timezone. Two people in different states then agree about a New Year’s Eve event.
| Where | Year |
|---|---|
| full, long, medium. Standing alone | Dropped when it is the current year |
| full, long, medium. In a list or table | Always shown. Siblings may span years |
| short | Never. That is what the style means |
| iso | Always |
| Range, both ends same year | On the later end only |
| Range straddling a year | Both ends |
context defaults to 'list'. A forgotten context yields a slightly verbose date. Never an ambiguous one.
Lowercase, minutes always, and no space before the meridiem. That last part is a deliberate departure: the platform’s own short time spaces it. Call sites never need a defensive toLowerCase().
7:30pm7:30pm AEDT7pm (short only)
Do
short is the single documented exception, for rows where the time is incidental.7:30 pm7:30 PM7.30pm7:30:00pm
Don’t
numeric and iso.A comma joins a date to its time. The word to joins the two ends of a range. A middot · separates a date from an adjacent fact. Never a dash of any kind.
Fri 27 Nov 2026, 7:30pmFri 27 to Sun 29 Nov 2026Fri 27 Nov 2026 · 3 days
Do
12:00pm - 10:00pm · Sun 29 NovFri 27 – Sun 29 Nov 2026
Don’t
A time earns its place when the reader can act on it, or when it changes a decision. Otherwise the date is the unit of meaning and the time is noise.
Doors 7:30pmOn sale Fri 27 Nov, 9:00am AEDTEdited 27 Nov 2026, 2:14pm
Do
Fri 27 to Sun 29 Nov 2026, 12:00amSettled 27 Nov 2026, 12:00amReport period: 1 Nov 2026, 12:00am
Don’t
An event time belongs to its venue. A timestamp belongs to whoever is reading it. timeZone is required so this is always a decision. There is no fallback to the browser.
Do
Fri 27 Nov from every state. An order reads in the hours the reader actually worked.new Date(startsAt).toLocaleDateString()format(startsAt, 'dd MMM yyyy')
Don’t
A moment belongs to a place, and which place depends on what kind of moment it is. Rule 7 turns on the distinction, so it is worth being explicit.
| Kind | Means | Timezone | Looks like |
|---|---|---|---|
| Event time | When something happens at a place. Doors, set times, the show. | The venue’s. Never converted. | Fri 27 Nov 2026, 7:30pm |
| Access time | When the reader must do something from wherever they are. On sale, presale, registration opening. | The venue’s, always labelled. | Fri 27 Nov, 9:00am AEDT |
| Timestamp | When something was recorded. An order, an edit, a note. | The reader’s. | 27 Nov 2026, 2:14pm |
A timestamp usually wants medium. The weekday of an audit entry tells the reader nothing. An event time usually wants long or full, because the weekday is often the whole reason someone is looking.
The usual advice is to convert everything into the reader’s local zone. That is right for a meeting, where each attendee joins from wherever they are sitting. It is wrong here.
A buyer will be standing at the venue when the doors open. Converting an 8pm Perth show into 11pm for a reader in Sydney does not save them arithmetic. It tells them the wrong time. The venue’s clock is not a formatting preference. It is the answer.
The test is where the reader will be when the moment arrives.
Doors 8:00pmOn sale Fri 27 Nov, 9:00am AEDTOrder placed 2:14pm
Do
Doors 11:00pm (a Perth show, read from Sydney)On sale 9:00amOrder placed 2:14pm AWST
Don’t
A national on sale is the one case where a second clock earns its place. The reader is at their own computer, competing for tickets. Lead with the venue’s zone, because that is what the promoter announced, and offer theirs after it.
On sale Fri 27 Nov, 9:00am AEDT
6:00am your timeThe most common silent failure in any timezone code is a conversion that lands on a different calendar day. Whenever a second clock is shown and it falls on another date, say so. Never show a bare time that is quietly a day out.
A reader who sees only a time assumes it is the same day. Half the time it is not.
10:00pm AEDT7:00pm your time12:30am AEDT9:30pm your time, Thu 26 Nov
Do
12:30am AEDT9:30pm your time
Don’t
A date with no time does not belong to a zone and must never be shifted by one. A public holiday, a birthday, an on-sale date with no announced hour: the same date everywhere. Converting it is how a date silently becomes the day before.
A common recommendation is to pair the abbreviation with its offset, 9:00am AEST (UTC+10), because abbreviations collide internationally: CST is Central US, China and Cuba.
Australian abbreviations do not collide with each other, and the audience reads them fluently. People do not think in offsets, so an offset is noise for almost every reader. Add one only where the audience is genuinely international, and never in place of the abbreviation.
AEST and AEDT differ by one character and one hour. For half the year Queensland and New South Wales sit on opposite sides of it. A reader in Brisbane has to catch a single letter to know the time is not their own.
Usually the place is already on the page. An event page names its venue; a listing names a city. Where a time appears without one beside it, name the place too.
The abbreviation is a confirmation, not an introduction.
The Lyrebird, Richmond VICDoors 8:00pm AEDTOn sale 9:00am AEDT, Sydney8:00am your time
Do
On sale 9:00am AEDTOn sale 9:00am Australia/SydneyOn sale 9:00am AET
Don’t
Do not reach for Intl’s generic zone names to solve this. shortGeneric renders Sydney as AET and Brisbane as AEST, so it removes the daylight saving distinction from one and not the other, making the two states harder to tell apart rather than easier.
A buyer is usually local to the event they are looking at. Someone running events often is too, and just as often is not: the same desk handles venues in several states. They also set the on-sale times thousands of buyers act on, so a zone read wrong here is an incident rather than an inconvenience.
Two rules bind harder here. The zone is never optional, because you cannot tell from the screen whether the reader is in it. And a second clock stops being a nicety: where the reader’s zone differs from the venue’s, show both, venue first.
// The viewer's zone is a client fact, so resolve it after mount
// rather than during render, where the server would disagree.
const viewer = useViewerTimeZone()
<DateTime at={startsAt} timeZone={venue.timeZone} timeStyle='long' />
{viewer && viewer !== venue.timeZone && (
<span className='text-subtle'>
<DateTime at={startsAt} timeZone={viewer} dateStyle='medium' timeStyle='medium' />
{' your time'}
</span>
)}An operator tool that prints an identifier or a raw offset is printing a database value, not copy.
Fri 27 Nov 2026, 7:30pm AWST27 Nov 2026, 9:30pm your time
Do
27 Nov 2026 07:30 PM Australia/Perth27 Nov 2026, 07:30 PM (GMT+8)27 Nov 2026 07:30 PM
Don’t
One thing must never happen here: an event time rendered in the reader’s zone as the primary value. That is the wrong answer wearing the right label, and the most common way a scheduling mistake reaches a buyer.
Australia has five zones and only some states observe daylight saving. For half the year a show in Brisbane and a show in Sydney are an hour apart. For the other half they are not. That is why timeZone is required, and why it takes an IANA identifier rather than an abbreviation.
| Abbreviation | Where | Daylight saving |
|---|---|---|
| AEST / AEDT | NSW, VIC, TAS, ACT | Observes DST |
| AEST | QLD | No DST |
| ACST / ACDT | SA | Observes DST |
| ACST | NT | No DST |
| AWST | WA | No DST |
| LHST / LHDT | Lord Howe Island | Half hour DST shift |
Show the zone when the reader might not be in it. An event page for a national tour, an on-sale time, anything someone in another state acts on. Leave it off when the reader is obviously local, because it is noise.
An abbreviation is what people recognise. A UTC offset is not.
On sale Fri 27 Nov, 9:00am AEDT
Do
On sale Fri 27 Nov, 9:00am GMT+11On sale Fri 27 Nov, 9:00am Australia/Sydney
Don’t
A late show is the normal case in this business, not an edge case. Two rules follow from that.
First, a set that ends at or before 6am belongs to the night before. A show running 10pm to 3am is one night out, not two. formatDurationDays applies this, so a festival does not gain a day on one screen and not another.
Second, 12:00am is ambiguous to a lot of readers. In prose, prefer the word. In a table or a row of times, keep the digits so the column aligns.
The formatter emits digits, which is right for a table. A sentence is different.
Doors 8:00pm, close midnightSat 28 Nov, 12:00am
Do
Doors 8:00pm, close 12:00amCloses 12 pm
Don’t
12 pm that half of readers will take for midnight.formatRelative renders a timestamp as elapsed time. It falls back to the absolute date once the moment is further away than the cutoff, which defaults to seven days.
formatRelative(note.addedAt, { timeZone: viewerTimeZone() })
// → 'just now' · '3 minutes ago' · '5 hours ago'
// → 'yesterday' · '3 days ago'
// → 'Fri 27 Nov 2026' once it is past the cutoffA relative time cannot be checked against a calendar or quoted to anyone. Put the absolute in a title or tooltip.
<DateTime at={note.addedAt} timeZone={viewerTimeZone()} relative />
Do
title, sets the machine value, and swaps the text in after mount so it neither goes stale nor mismatches the server.<span>{formatRelative(at, o)}</span><time dateTime={iso}>{formatRelative(at, o)}</time>
Don’t
It reads the future too, so in 20 minutes works for a door opening. Not for the event itself: “in 3 days” is a worse answer than Fri 27 Nov for something a person has to turn up to.
Past and future are not mirror images. For something that already happened, closer means relative is better. For something that has not happened yet, closer means relative is worse.
“3 minutes ago” beats a timestamp on an audit row. But “in 4 hours” is a bad answer for a sale opening today, because you cannot set an alarm from it. As a future moment gets nearer, hand back the actual time.
This is what formatRelative does. The cutoff defaults to seven days.
| Distance | Reads |
|---|---|
| Under a minute | just now |
| Under an hour | 3 minutes ago |
| Under a day | 5 hours ago |
| One day | yesterday |
| Under a week | 3 days ago |
| Older | Tue 17 Nov 2026 |
The inverse, and the formatter does not do it for you. Build it from the distance, the way an on-sale label does.
| Distance | Reads | Why |
|---|---|---|
| Over a week | Mon 7 Dec 2026 | A date. Nobody counts down from nine days. |
| Under a week | in 5 days | Close enough to feel soon. |
| One day | tomorrow | The word beats the number. |
| Today | 9:00am AEDT | Absolute. You cannot set an alarm from “in 4 hours”. |
The default suits a note or an audit row. Shorten it where staleness matters, and do not reach for it at all where someone is making plans.
| Surface | Cutoff | Why |
|---|---|---|
| Notification, activity feed | 1 hour | Anything older wants a real time. |
| Audit row, note, comment | 7 days | The default. |
| “Last updated” on a report | 7 days | Recency is the whole signal. |
| Anything a person turns up to | Do not use it | A countdown is not a plan. |
Ask what the reader does next. If the answer involves a calendar or an alarm, give them a date and a time.
Edited 3 minutes agoOn sale in 5 daysOn sale tomorrowOn sale at 9:00am AEDT
Do
Edited 27 Nov 2026, 2:14:03pmOn sale in 4 hoursStarts in 3 daysOn sale in 63 days
Don’t
Charts and dense tables are scanned and compared, not read. In prose a date is a fact in a sentence. In a column it is a value to line up against the ones above and below it.
So the rules bend in one specific direction: fixed width, no decoration, and nothing that repeats what the surrounding structure already says.
| Where | Use | Reads | Why |
|---|---|---|---|
| Axis tick | dateStyle 'short' + timeStyle 'numeric' | 27 Nov 19:30 | Dozens on screen at once. No weekday, no year, no meridiem. |
| Tooltip | dateStyle 'long' + timeStyle 'medium' | Fri 27 Nov 2026, 7:30pm | Restores everything the tick dropped. That is what makes the tick safe. |
| Table column, date identifies the row | dateStyle 'medium' | 27 Nov 2026 | An order or settlement date. The weekday is not what the reader is comparing. |
| Table column, weekday is a variable | dateStyle 'long' | Fri 27 Nov 2026 | An events list. The weekday is a dimension the reader analyses down the column. |
| Table column, with time | + timeStyle 'numeric' | 27 Nov 2026 19:30 | Two fixed-width fields. The column aligns and sorts. |
| Export cell | dateStyle 'iso' + timeStyle 'numeric' | 2026-11-27 19:30 | Sorts lexicographically. Unambiguous in any locale. |
| Sort or group key | Not a display format | not shown | Key on the instant. A formatted string is not a key. |
A date in a column is usually a label: it says which row this is, and the reader is comparing the numbers beside it. There, the weekday is repetition and medium is right.
Sometimes the weekday is the thing being compared. A promoter reading their events list wants to know whether Wednesdays sell worse than Fridays, and the only way to see that is to scan the weekday down the column against the figures. There it is an axis of the analysis. Dropping it removes the pattern.
The test is not how wide the column is. It is whether the answer to their question is in that token.
Events list Fri 27 Nov 2026Orders table 27 Nov 2026Settlements 27 Nov 2026
Do
Events list 27 Nov 2026Orders table Fri 27 Nov 2026
Don’t
When it is a dimension, keep it leading and abbreviated. Three characters in a fixed position is what lets the eye run down the column and catch the repetition. A weekday buried mid-string, or spelled out at varying widths, cannot be scanned that way.
An axis tick drops the weekday, the year and the meridiem because the axis range establishes all three and because dozens of ticks share the width of one chart. That is only safe because hovering restores them. Design the pair together or neither works.
A reader aims at a date, never at a two-pixel line. The tick orients; the tooltip answers.
tick: 27 Nov 19:30tooltip: Fri 27 Nov 2026, 7:30pm
Do
tick: Fri 27 Nov 2026, 7:30pmtooltip: 27 Nov 19:30
Don’t
numeric is where rule 4 gives way for a reader, the way iso does for a file. A meridiem costs three characters on every tick, and worse, it changes width across midday, so 11:45am and 12:00pm crowd differently and a chart library starts dropping ticks unevenly.
It also matches the source. Operators read a scan chart against a run sheet, and run sheets are 24 hour. A buyer should never meet this style.
Grouping, sorting, bucketing and caching key on the instant, never on the string that gets displayed. A formatted date is an output. The moment one is used as a key, changing how a date looks changes which rows group together, and that failure is silent.
The scale is day first, never month first. 27 Nov 2026, not Nov 27, 2026. This is not a preference. A month first date is read wrongly by most people here.
These follow from the scale. You only break them by hand rolling a date.
Fri 27 Nov 20267:30pmAnzac Day, Boxing Day
Do
Nov 27, 2026Fri 27th Nov07:30pm
Don’t
numeric and iso.Residencies, weekly nights and seasons all need plain words. Several common ones mean two different things to two different readers.
If a word has two readings, a promoter and a buyer will pick different ones.
Every 2 weeksTwice a monthThursdays, 5 Nov to 17 Dec
Do
FortnightlyBimonthlyBiannual
Don’t
Ranges need nothing extra. They are joined with the word to, so they read correctly on screen and out loud. A dash would not. Screen readers announce an en dash inconsistently and often skip it, which turns a range into one run on date.
Seat runs are the exception and keep a hyphen. A1-4 is a compressed list, not a range someone reads as a sentence, and A1 to 4 would suggest four separate seats. The brand guide already spells number ranges this way, and it is the easier character to type.
The rules are portable. The implementation is not. There are two cases and they take different answers.
Use the formatters. Each component page lists the ones behind it under Without React, with the call sites they are for: DateTime, Duration, Countdown and CalendarTile. formatMachine gives you the attribute value, so there is no reason to build one by hand.
The application owns its own formatter and follows the same rules, with the same style names. Name its methods after the style, never the format string they produce, so a call site cannot pick the wrong one.
This is the only case that writes the time element by hand. The attribute is the machine value. Three shapes cover it.
| Showing | datetime | Why |
|---|---|---|
| A date | 2026-11-27 | A calendar date has no zone. Do not invent one. |
| A date and time | 2026-11-27T19:30:00+10:00 | The offset is what makes it an instant. |
| A time only | 19:30 | Rare. Only where the date is already established. |
A local time with no offset is a floating value. It parses to a different instant in every timezone that reads it.
datetime="2026-11-27T19:30:00+10:00"datetime="2026-11-27"
Do
datetime="2026-11-27 19:30"datetime="Fri 27 Nov 2026, 7:30pm"
Don’t
Do not reuse the iso style here. It has no offset and uses a space rather than a T, which is right for a spreadsheet cell and wrong for this attribute. See DateTime.
Skip relative time on the server. It is stale the moment it is sent, and there is nothing to re-render it.