Components · Forms

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.

Stable · v1.0 shadcn/ui ISO 8601 WCAG 2.2 AA

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

Date field with calendar aid
YYYY-MM-DD · local time, Europe/Vienna
August 2026 CEST
Mo
Tu
We
Th
Fr
Sa
Su
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
selected 2026-08-11 · today 2026-08-13 · future dates disabled
ElementRule
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.

ElementRule
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

ISO 8601, always

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.

Timezone is part of the value

A clinical timestamp without a zone is not a timestamp. Three rules:

The two ambiguous hours

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 advice this section exists to reject

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:

The rule

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.

KindCaptured as Displayed as
ExactTime, to the minute02:15
ApproximateTime plus a stated precision about 14:00 (to the nearest hour)
BoundedTwo 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
UnknownExplicitly unknown, with what was asked unknown (patient unable to say; no witness)
DisputedEvery 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.

An interval that straddles a decision boundary
Onset between 22:00 and 06:30 unwitnessed
Now07:10
Time since onset between 0 h 40 min and 9 h 10 min

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 kindConstraintBehaviour
Observation — acquisition, sample drawn Not in the future Future dates disabled in the calendar; typed future dates rejected with the reason.
Date of birthNot in the future; plausible age range Outside the range confirms rather than rejects — see Text field.
Scheduled — repeat sampling, follow-upUsually future Past dates permitted but confirmed, since back-dating is sometimes correct.
Range — trend windowStart ≤ 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.

BehaviourVerdict
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.

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

Relative and absolute time

"4 min ago" is easier to read and harder to verify. The rule follows the consequence:

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.

RuleHeld by How
Imprecision is a value, never a blankThe 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 zoneThe 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 impliedThe 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 yearThe 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 correctedThe 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 completedThe 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 blankThe 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 UTCYou 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 careYou 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 resolvedYou 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 observationYou There is no kind on this component. Nothing here knows whether the field records something observed or something planned.
Why this is written down

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

Do
YYYY-MM-DD HH:MM:SS · Europe/Vienna · cannot be in the future

Unambiguous format, named zone, constraint stated before entry.

Don't
Pick a date

Locale-ambiguous order, two-digit year, am/pm, no zone, no constraint. Four ways to record the wrong moment.

Do
Recorded as not known. Anything derived from it will be reported as underivable.

Unknown is stored as a value with a reason. A reader years later can tell it was asked and not answered.

Don't
Leave blank if the date is unknown.

The blank now means unknown, or unasked, or abandoned, or lost in transfer. Four situations, one rendering, and no way back.

Do

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.

Don't

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.

Do
YYYY-MM-DD · or choose from a calendar

Typed in three seconds by anyone who knows it. The calendar is there for anyone who does not, with year selectable directly.

Don't

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.

Do
Amiodarone from 2027-03-12, ongoing at least 14 days

Open at one end, stated as such, and the derived duration inherits the openness rather than pretending to a figure.

Don't
Amiodarone 2027-03-12 — 14 days

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

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.

Clinical safety notes

Risk controls this page specifies

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.

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.
PropTypeDefaultNotes
labelstring— Required. The fieldset legend; each inner control is named from it.
valueClinicalTime | 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.
timeZonestring (IANA)— Required. The site of care, never the browser. Rendered beside the value.
allowPrecision[]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.
hintstring— 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.

NotJustAnyMed.Tech Design System · Date & time picker · v1.0 · draft for review
Reference applications named in this system are fictional; all patient data shown is fabricated.