Skip to content

DurationField

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/duration-field.json
See it liveFilled, empty, and inside a Field with a hint.

The alternative is two number boxes. Labelled “hours” and “minutes”, they mean two tab stops, two validations, and a reader who has to divide 90 minutes in their head before typing. Here they write it the way they say it.

<DurationField value={minutes} onValueChange={setMinutes} />

The value is minutes — a plain number, not a string and not a Duration object. A field whose value has to be parsed by its caller has moved the problem rather than solved it.

Loose going in, strict coming out. Everything below means ninety minutes, and all of them are written back as 1h 30m:

Typed Means
1h 30m, 1h30m the canonical spelling, spaced or not
90, 90m a bare number is minutes
1.5h, 1,5h a decimal, with either separator
1:30 the clock spelling

That asymmetry is the design: being strict on input means rejecting people, being loose on output means the column of values never lines up.

What it will not do is guess. 1h banana is refused rather than read as an hour — a typo that parses is a value nobody questions afterwards. When what was typed cannot be read, the box is put back to the value the form actually holds rather than left saying something untrue.

Empty is null. No estimate is not an estimate of nothing.

It does not reformat under the cursor. Typing 1h 3 leaves 1h 3 alone until you leave the field or press Enter; a field that reformats on every keystroke fights the person using it.

Prop Type Default
value number | null Minutes; null is empty
onValueChange (value) => void Fires on blur and Enter, not per key
placeholder string A duration in the canonical spelling reads best
disabled, readOnly, required boolean false

parseDuration and formatDuration are exported beside the component, for a product that has to read or write the same spellings elsewhere.