Date & time picker
Clinical time is not calendar time. It carries a timezone, it cannot run forwards, it has two ambiguous hours a year, and getting it wrong changes what a trend means.
Overview
Almost every clinical decision this system supports is a decision about change over time: ST elevation compared with a prior, troponin across a 0/1 h interval, how long a patient has waited. All of it rests on timestamps being unambiguous, ordered and correctly zoned — and a picker is where a human introduces the error.
Unambiguous
ISO 8601, 24-hour, with the timezone visible. A date that means two things depending on where you trained is not a date.
Bounded
Observations cannot be in the future. Constraints are stated before entry, not discovered after it.
Typeable
The calendar is an aid. A clinician who knows the date types it — six taps to reach last Tuesday is a failure of the control, not of the user.
The page runs in the order the problem does: what a clinical time is (format, zone, precision, and what follows when it is imprecise); how one is entered (constraints, typing help, distant dates); ranges, including those open at one end; and how time is read back.
Anatomy
| Element | Rule |
|---|---|
| Text field | The primary input. Accepts a typed ISO date and is never read-only in favour of the calendar. |
| Format hint | YYYY-MM-DD, always visible, plus the timezone the value is interpreted in. |
| Calendar | An aid. Marks today distinctly from the selection, and disables what is out of range rather than silently rejecting it. |
| Footer | Restates the selection in full, and names the active constraint. |
What ships, and what does not
The calendar above is the design; it is not built. What
installs is ClinicalDateTime, the field that carries the rules this
page is actually about — and the calendar aid, when it is built, sits beside it rather than
replacing it.
| Element | Rule |
|---|---|
| Precision selector | Asked first, because it changes what the rest of the control is asking for. Offering a time input and hiding “unknown” behind a checkbox makes the exact answer the path of least resistance, which is the whole problem. |
| Typed field | A text input, YYYY-MM-DD HH:MM, 24-hour, with the shape stated beneath it. Not a
native date input: those render in the browser’s locale, which is the ambiguity this page
exists to remove. An impossible value is reported on leaving the field and left exactly as
typed. Bounded times get two of these. |
| Unknown statement | When the precision is “not known” there is no field at all — a sentence saying so, and that anything derived will be reported as underivable. |
| Zone line | Names the zone the value is in. timeZone is required, so the line cannot be
absent. |
Format
04/12/57 is 4 December to a European clinician and 12 April to an American one,
and both readings are confident. Dates are entered, displayed, stored and printed as
YYYY-MM-DD throughout — no locale-aware formatting, no input masks that reorder
fields, no two-digit years. The same rule as
Patient header.
- 24-hour time.
14:26, never2:26 PM. Removes the am/pm class of error entirely. - Seconds where clinically relevant. ECG acquisition and alarm timestamps carry seconds; an appointment does not.
- Never abbreviate a month to letters. "Aug" does not translate and sorts wrongly.
- Tabular numerals, so timestamps align in a column — see Typography.
Timezone is part of the value
A clinical timestamp without a zone is not a timestamp. Three rules:
- Store UTC, display local, name the zone. The displayed zone is stated beside the value or in the field hint — never assumed from the browser without saying so.
- Display in the zone of the site of care, not the reader's. A cardiologist reviewing from another country must see the times the ward saw, or every interval they compute is wrong.
- Never mix zones in one view. If a record genuinely spans sites, each timestamp carries its zone explicitly and the mixing is stated.
At the end of daylight saving, one local hour occurs twice. A 0/1 h troponin interval spanning it can appear to run backwards, or to be two hours long. The picker resolves this by storing UTC and, where a local time is ambiguous, asking which occurrence is meant rather than guessing. At the spring transition, times inside the skipped hour are rejected with an explanation — they did not exist.
Precision is part of the value
The standard guidance for an unknown date is “leave blank if the date is unknown”, usually offered as instructional text beneath the field. It is a reasonable instruction in most software and wrong here for one reason: a blank field records nothing about why it is blank. Nobody downstream can tell an unknown date from a question never asked, a form abandoned halfway, or a value lost in transfer.
Unknown is a value, and it is stored as one. The same argument as Key–value pair, applied to input rather than display.
Everything above assumes the time is known. Clinically it very often is not, and the field that only accepts an exact time forces the person filling it in to invent one.
"Woke up with it" becomes 06:00. "Sometime after lunch" becomes 13:00. The invented digits are then indistinguishable from measured ones, and every calculation downstream inherits a confidence nobody ever had. So:
A time is stored with the precision it was known to, and an unknown time is a value rather than an empty field. A time recorded to the hour is not a time recorded to the minute whose minutes happen to be zero.
| Kind | Captured as | Displayed as |
|---|---|---|
| Exact | Time, to the minute | 02:15 |
| Approximate | Time plus a stated precision | about 14:00 (to the nearest hour) |
| Bounded | Two times — the interval it lies within | between 22:00 and 06:30 |
| Last known well | The latest time the person was known to be at baseline, and who says so | last known well 22:00 (partner, by phone) |
| On waking / unwitnessed | Bounded by sleep onset and waking, marked unwitnessed | on waking — between 22:00 and 06:30, unwitnessed |
| Unknown | Explicitly unknown, with what was asked | unknown (patient unable to say; no witness) |
| Disputed | Every reported value, each with its source | 02:00 (patient) · 23:30 (ambulance record) |
Disputed is not a rare edge case and must never be resolved by the interface picking one. Two sources disagreeing is a clinical finding in itself, and the software has no basis for preferring either.
What follows from an imprecise time
The reason this is a safety section rather than a data-modelling one is what happens downstream. Time-since is the input to decisions with hard boundaries, and an approximate time put through an exact calculation comes out looking exact.
- A derived interval inherits the widest uncertainty of its inputs, and is displayed as a range. Two approximate times do not produce a precise duration between them.
- An imprecise time is never compared against a threshold as though exact. Where an interval straddles a decision boundary the interface says it straddles it — it does not report the midpoint, the earliest, or the latest, because each of those is a clinical judgement being made silently by a rounding rule. This is the Derived measurement objection in the time domain.
- Precision survives export. An approximate time that arrives at another system as an exact timestamp has lost the only thing that made it honest — the same survivability test as Authorship.
- Unknown does not default. It does not become "now", it does not become the admission time, and it does not become blank. It stays unknown and everything derived from it is stated as underivable.
Cannot be determined against the protocol window. The interval spans it. This is a clinical decision, not a calculation.
The temptation is to show "9 h 10 min" because it is the safe end, or "0 h 40 min" because it is the one the clinician is hoping for. Both are the software answering a question it was not asked, and the honest output is the one that hands the decision back.
Constraints
| Field kind | Constraint | Behaviour |
|---|---|---|
| Observation — acquisition, sample drawn | Not in the future | Future dates disabled in the calendar; typed future dates rejected with the reason. |
| Date of birth | Not in the future; plausible age range | Outside the range confirms rather than rejects — see Text field. |
| Scheduled — repeat sampling, follow-up | Usually future | Past dates permitted but confirmed, since back-dating is sometimes correct. |
| Range — trend window | Start ≤ end | Selecting an end before the start reorders them and says so; it never silently swaps. |
Constraints are stated before entry, not after. A hint reading "not in the future" costs nothing; discovering it after typing costs a re-entry at the worst moment.
What typing help may and may not do
Date fields attract input assistance — a separator appearing after two digits, a single digit padded to two, a pasted string tidied up. Some of it is genuinely useful. The line runs between completing what the clinician typed and interpreting what they meant.
| Behaviour | Verdict |
|---|---|
| A separator appears after the year and the month | Permitted. It completes the pattern the field already declared and adds no information. |
A single digit is padded — 3 becomes 03 |
Permitted, on leaving the segment, never mid-keystroke. Rewriting characters under a moving cursor fights the typist and is announced as a change by a screen reader on every stroke. |
An out-of-range number is corrected — month 13 becomes
12 |
Never. The clinician typed something impossible; the field says so. Silently choosing the nearest legal value invents a date nobody entered. |
An ambiguous order is inferred — 03/04 read as one or the other |
Never, and it cannot arise here. The field is ISO throughout, so there is no order to infer. This is the failure that rule exists to remove. |
| A pasted value in another format is parsed | Permitted, and echoed for confirmation. Parsing is a convenience; the result is shown in full, unambiguous form and left for the clinician to accept. Never parsed and committed silently. |
| The field reformats as the clinician types | Never. Formatting settles on blur. A value that changes shape under the cursor cannot be checked while it is being entered. |
The general form: assistance may add what the format already implies. It may never choose between readings. A date field that guesses is a date field that will sometimes guess wrong, quietly, in a record.
Reaching a date that is far away
A calendar widget is an excellent instrument for next Tuesday and a poor one for the second of March 1954. Roughly six hundred taps of a month caret, or a month-and-year selector nobody notices, stand between a clinician and a date of birth — which is among the most frequently entered dates in clinical software.
- Typing is the primary route, always. A clinician who knows the date types it. The calendar is the aid, and a field that can only be filled from a calendar is a field that is slow for everyone who already has the answer.
- The calendar is never behind an icon alone. This system has no icon set — see Contributing — so the control that opens it is a labelled button, and it is not the only way in.
- Month and year are selectable directly, not reachable only by stepping. A year list beats twelve caret presses and is the difference between a date of birth taking three seconds and thirty.
- Never a two-digit year, and never an inferred century.
54is 1954 or 2054 depending on a rule the clinician cannot see, and for a date of birth both are occasionally plausible. - Out-of-range dates are disabled and say why, per Constraints. A greyed cell with no reason is a dead end the reader has to hypothesise about; "not in the future" costs one line.
Ranges
A range is two values and one meaning, and the meaning is where it goes wrong. "Last 7 days" on Monday is a different set of data from "last 7 days" on Tuesday, and a chart captioned with the relative phrase cannot be compared with the one a colleague screenshotted yesterday.
- A relative range always resolves to absolute dates on screen — "Last 7 days · 10–17 Aug 2027". The relative phrase is a convenience for setting it, never the record of what was set.
- The boundaries state whether they are inclusive, and whether the range is by event time or recorded time, per Clinical timeline. An off-by-one day at the boundary of a follow-up window is not a rounding error.
- An impossible range is refused at entry, not accepted and reported as empty — an end before a start returns nothing, which is indistinguishable from a period in which nothing happened.
- The range travels with anything derived from it. A count, a chart or an export carries the period it covers in its own label, because it will be read somewhere the picker is not.
- Presets never silently clamp. If "last 12 months" exceeds the retention period or the record's own start, the range shown is the one actually used, with the difference stated.
A range with one end open
Clinically ordinary and absent from most date-range components: a medication still being taken, an admission that has not ended, a symptom that started and continues. The range has a start and no end, and that is a fact rather than a gap in the form.
- Ongoing is a value, not an empty second field. "From 12 Mar 2027, ongoing" is a complete answer. An end field left blank is indistinguishable from one nobody filled in — the same argument as unknown times, one field along.
- Ongoing and unknown are different. A medication still being taken and a medication whose stop date nobody recorded are opposite clinical situations, and they render differently.
- Anything derived says which it used. A duration computed from an ongoing range is "at least 14 days", not "14 days" — the interval is open at one end and the number inherits that.
- Both ends share one format, which follows from ISO throughout and is worth stating because mixed formats across two adjacent fields is a common and quiet defect.
Relative and absolute time
"4 min ago" is easier to read and harder to verify. The rule follows the consequence:
- Relative time for elapsed duration a clinician is monitoring — waiting time in a queue, countdown to a re-alert. It answers "how long", which is the actual question.
- Absolute time for anything recorded — acquisition, acknowledgement, assessment, notification. The audit trail is absolute, always, with seconds.
- Never relative alone on a clinical event. Where relative is shown for
readability, the absolute time sits beside it:
4 min ago · 14:26:38. - Relative time updates live, or it is a lie within a minute — but the update never animates and never reflows the row.
What the component enforces, and what it only asks for
The component decides how a time is written down and how imprecision is recorded. It does not decide what your system stores, and the rules about storage are the ones with the longest reach.
| Rule | Held by | How |
|---|---|---|
| Imprecision is a value, never a blank | The component | ClinicalTime is a union with cases for approximate, bounded, unknown and disputed. There is no way to represent “not known” as an empty field.Asserted: ClinicalDateTime: unknown is a stated value, not a blank field |
| A timestamp always names its zone | The component | timeZone is required. A clinical time cannot be rendered without one.Asserted: ClinicalDateTime: the zone is named beside the value |
| Precision is displayed, not implied | The component | An hour-precision time renders as an hour-precision time rather than one with invented minutes. Asserted: ClinicalDateTime: precision is asked before the value |
| The value is written as ISO 8601, 24-hour, four-digit year | The component | The field is a text input with the shape stated beneath it, not a native date input that renders 08/13/2026 on one workstation and 13/08/2026 on the next.Asserted: ClinicalDateTime: the value is typed as ISO 8601, never a locale-rendered native picker |
| An impossible value is reported, never corrected | The component | Month 13 stays month 13, marked invalid with the shape it should have had. The field pads nothing, reorders nothing and never picks the nearest legal date. Driven in a browser. Asserted: ClinicalDateTime: an impossible value is reported on leaving the field, and left as typed |
| Every precision offered can be completed | The component | Approximate asks how approximate; a bounded time asks whether it was witnessed; last known well asks who says so; sources disagree takes a time and a source per report. Six precisions were offered and two could be completed: the others recorded “to the nearest hour”, “unwitnessed”, an empty source and no reports, whatever was meant. Driven in a browser. Asserted: ClinicalDateTime: every precision it offers can be completed |
| A formatted time is never blank | The component | formatClinicalTime gave an empty string for a disputed time with no reports and “()” for a missing source. Both say what is missing now.Asserted: ClinicalTime: a disputed time and an unsourced last-known-well never format to a blank |
| Values are stored as UTC | You | The component takes and returns a ClinicalTime. What your API and database do with it is outside this page. |
| Times display in the zone of the site of care | You | You pass timeZone. The component renders what you give it, including the reader's browser zone if that is what you pass — which is the failure the rule exists to prevent. |
| The ambiguous hour at a daylight-saving transition is resolved | You | Asking which occurrence is meant, and rejecting times in the skipped hour, is validation in your layer. The component has no calendar of transitions. |
| A future time is refused where the value is an observation | You | There is no kind on this component. Nothing here knows whether the field records something observed or something planned. |
A rule stated on a component page reads as a rule the component keeps. 5 of these 9 are; 4 are obligations this page was quietly handing to the reader. Every rule about storage is yours, and storage is where a timestamp does its damage: an interval computed across two zones is wrong long after the screen that produced it has gone.
Do's and don'ts
Unambiguous format, named zone, constraint stated before entry.
Locale-ambiguous order, two-digit year, am/pm, no zone, no constraint. Four ways to record the wrong moment.
Unknown is stored as a value with a reason. A reader years later can tell it was asked and not answered.
The blank now means unknown, or unasked, or abandoned, or lost in transfer. Four situations, one rendering, and no way back.
Typed 2027-03-1 → on leaving the field, 2027-03-01
Completes what the format already implied, once, when the clinician has finished. Nothing moved under the cursor.
Typed 2027-13-04 → silently stored as 2027-12-04
The clinician typed something impossible and the field chose the nearest legal date. It now holds a value nobody entered and nothing says so.
Typed in three seconds by anyone who knows it. The calendar is there for anyone who does not, with year selectable directly.
Date of birth — read-only field, calendar opens from an icon, month stepped one at a time.
Roughly six hundred presses to reach March 1954, behind a glyph with no label, on one of the most frequently entered dates in clinical software.
Open at one end, stated as such, and the derived duration inherits the openness rather than pretending to a figure.
An empty end field, and a duration computed as though today were the stop date. Still-being-taken and nobody-recorded-the-stop now look identical.
Accessibility
- The text input is the control; the calendar supplements it. A picker that can only be operated by clicking a grid is unusable for many keyboard and switch users, and slow for everyone who knows the date.
- Full keyboard grid — arrows move by day, Page Up/Down by month, Home/End to week bounds, Esc closes and returns focus to the field.
- Each day has a full accessible name — "Tuesday 11 August 2026", not "11" — and disabled days announce why.
- Today and selected are visually distinct and distinguished by more than colour: today by border, selection by fill plus weight.
- Touch targets obey the physical floor from Scaling, which on a cart means a larger calendar, not smaller days.
- Never
type="date"ortype="datetime-local"where the format matters — their rendering and behaviour vary by browser and locale, which is precisely the ambiguity this page exists to remove. The shipped field is a text input with the shape stated beneath it and announced with the field.
Outcomes of use
What this contributes to, in the terms of Usability & context of use. These are attributes believed to contribute to an outcome; the outcome itself is settled by observing real use in a specified context, not by this page.
- Effectiveness — the recorded moment is the moment that happened. Every interval, trend and audit entry downstream depends on it.
- Efficiency — typing a known date beats navigating to it. The calendar exists for the cases where the clinician is counting backwards from today.
- Satisfaction — no second-guessing whether the field meant day-month or month-day, which is a small, constant tax in any multinational deployment.
Clinical safety notes
Trace these in your risk file (ISO 14971) and usability engineering file (IEC 62366-1). Each says who holds it, matching the guarantees above — a control the component does not keep is one your risk file must show you keep.
- ISO 8601 and 24-hour only (the component). Mitigates: day/month transposition and am/pm error — the two most common date entry faults in multinational deployments.
- Timezone named (the component) and stored as UTC (you). Mitigates: intervals computed across zones being wrong by hours.
- Ambiguous DST hour resolved by asking (you). Mitigates: an interval appearing to run backwards, or a duplicate-hour sample being ordered wrongly.
- Observations cannot be future-dated (you). Mitigates: an impossible timestamp corrupting a trend or an audit sequence.
- Constraints stated before entry (you; the component states its format and zone). Mitigates: re-entry under time pressure.
- Absolute time on every recorded event (you). Mitigates: an audit trail that cannot be reconstructed later.
- Range order corrected explicitly, never silently (you). Mitigates: a trend window inverted without the clinician knowing.
- Imprecision recorded as a value (the component). Mitigates: an invented onset time putting a patient on the wrong side of a treatment window.
- Impossible values reported, never corrected (the component). Mitigates: a record holding a date nobody entered.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/date-picker.json
<ClinicalDateTime
label="ECG acquired"
timeZone="Europe/Vienna" // required: the site of care, never the browser
value={value}
onChange={setValue}
allow={["exact", "approximate", "unknown"]}
/>
// 'timeZone' is a required prop, so a clinical timestamp cannot be rendered
// without one. That is the whole enforcement — the component does not throw.
| Prop | Type | Default | Notes |
|---|---|---|---|
label | string | — | Required. The fieldset legend; each inner control is named from it. |
value | ClinicalTime | null | — | Required. null is “not yet answered”, which is not the same
value as { kind: "unknown" } — see Precision. |
onChange | (v: ClinicalTime | null) => void | — | Required. Called with the whole value, including its kind. |
timeZone | string (IANA) | — | Required. The site of care, never the browser. Rendered beside the value. |
allow | Precision[] | all six kinds | Restricting to ["exact"] is legitimate for a machine-generated timestamp and is a
design decision to record. For anything a person reports, offering only exact forces them to
invent digits. |
hint | string | — | Prepended to the zone line. |
Also exported: formatClinicalTime, which carries the precision into the words and is the
only sanctioned way to print one; isIsoShaped, the shape test the field applies on blur;
and formatInterval / intervalDuration for
ranges with one end open.
Related
- Affordance & perceivability — the general rule this page applies: a control exists only if every reader who can operate it can perceive it.
- Clinical timeline — where these times are read together, and why event time and recorded time are not interchangeable.
- Text field — the input underneath, and its validation rules.
- Patient header — the same date formatting.
- Typography — tabular numerals.
- Clinician override — where absolute timestamps land.