Duration input
How long something lasts, typed as the crew would say it and written back in the field's units.
On this page
The page receives: 240 minutes
Code
import { DurationInput } from "@bosunkit/components/duration-input";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"A watch length held in minutes: typed as the crew say it, written back in hours and minutes, stepped by 30 min.";
export default function Showcase(): ReactElement {
const [minutes, setMinutes]: [number | null, Dispatch<SetStateAction<number | null>>] = useState<number | null>(240);
return (
<div style={{ display: "grid", gap: "var(--space-sm)", maxInlineSize: "22rem" }}>
<DurationInput
appearance="outlined"
label="Watch length"
unit="min"
min={60}
max={480}
step={30}
value={minutes}
onValueChange={setMinutes}
hint="Between 1 and 8 hours."
/>
<p style={{ margin: 0, color: "var(--text-dim)" }}>
The page receives: {minutes === null ? "no duration" : `${String(minutes)} minutes`}
</p>
</div>
);
}
A duration input holds how long something lasts: a watch, an alarm delay, a service interval, a timer. The crew type
it as they would say it, in any of the six languages: "4h30", "4:30", "1,5 h", "2 Std 15 Min", "3 uur". A bare
number is in the field's unit, the one bosund stores, so "270" in a minutes field is 270 minutes. Two numbers with
a colon are hours and minutes, and in a seconds field minutes and seconds, as a timer shows them: "1:30" is 90
seconds there. When they press Enter or leave the field it is written back in its fields ("4 hr 30 min"), each
unit in the language's own abbreviation from the browser's CLDR data, and the smallest floored, so it never says
more time than it holds.
It is a spin button: the arrow keys step it by step, Page Up and Page Down by ten steps, Home and End to min and
max, and a screen reader hears the value in words ("4 hours, 30 minutes"). With a step it also draws a shorter and a
longer button, each named by how far it moves. A duration it cannot read, or one outside its limits, keeps the typed
text and says what to type; nothing is reported until it is right. presets offers durations as chips. Outlined, the
field's name sits on its outline; plain, it takes its name, hint and error from a FormField. In a schema form a number
field tagged component=duration with a unit of time draws it.
Examples
Presets
Code
import { DurationInput } from "@bosunkit/components/duration-input";
import type { ReactElement } from "react";
export const description: string = "A watch length held in hours, with the three lengths a rotation uses as chips.";
export default function Presets(): ReactElement {
return (
<div style={{ maxInlineSize: "22rem" }}>
<DurationInput appearance="outlined" label="Watch length" unit="h" defaultValue={3} presets={[2, 3, 4]} />
</div>
);
}
Seconds
Code
import { DurationInput } from "@bosunkit/components/duration-input";
import type { ReactElement } from "react";
export const description: string = "An anchor alarm's delay, held in seconds and written in minutes and seconds.";
export default function Seconds(): ReactElement {
return (
<div style={{ maxInlineSize: "22rem" }}>
<DurationInput appearance="outlined" label="Alarm delay" unit="s" defaultValue={90} step={15} min={0} max={600} />
</div>
);
}
In a form field
Engine hours between oil changes.
Code
import { DurationInput } from "@bosunkit/components/duration-input";
import { FormField } from "@bosunkit/components/form-field";
import type { ReactElement } from "react";
export const description: string = "Plain, inside a form field that gives it its label and hint.";
export default function Plain(): ReactElement {
return (
<div style={{ maxInlineSize: "22rem" }}>
<FormField label="Service interval" hint="Engine hours between oil changes.">
<DurationInput unit="h" defaultValue={250} fields={["h"]} />
</FormField>
</div>
);
}
Outside its limits
Code
import { DurationInput } from "@bosunkit/components/duration-input";
import type { ReactElement } from "react";
export const description: string = "A caller's error under the outlined field, as a form shows one bosund returned.";
export default function Invalid(): ReactElement {
return (
<div style={{ maxInlineSize: "22rem" }}>
<DurationInput
appearance="outlined"
label="Rest between watches"
unit="min"
defaultValue={90}
error="The rotation needs at least 2 hr between watches. Make it longer."
/>
</div>
);
}
Read-only
Code
import { DurationInput } from "@bosunkit/components/duration-input";
import type { ReactElement } from "react";
export const description: string = "Read-only: the value without step buttons or presets.";
export default function ReadOnly(): ReactElement {
return (
<div style={{ maxInlineSize: "22rem" }}>
<DurationInput appearance="outlined" label="Engine run time" unit="min" defaultValue={135} readOnly step={5} />
</div>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | null | undefined | – | The duration in unit, when the caller holds it; null for none. A fraction is allowed (1.5 in hours). |
| defaultValue | number | null | undefined | – | The duration it starts with when it holds the value itself. |
| onValueChange | ((value: number | null) => void) | undefined | – | Called with the duration in unit when the crew commit one (Enter, leaving the field, a step or a preset). |
| unit | DurationUnit | undefined | "min" | The unit the value is held in, and a bare typed number is read in; min unless set. |
| fields | readonly DurationUnit[] | undefined | – | The units the duration is written in, any of d, h, min, s; by unit: days and hours for d, hours and
minutes for h and min, minutes and seconds for s. The smallest is floored, so it never says more time. |
| min | number | undefined | – | The shortest duration allowed, in unit; a shorter one is refused with a message. |
| max | number | undefined | – | The longest duration allowed, in unit; a longer one is refused with a message. |
| step | number | undefined | – | How far the step buttons and the arrow keys move it, in unit; no step buttons unless set. |
| presets | readonly number[] | undefined | – | Durations offered as one-press chips under the field, in unit ("2 hr", "3 hr", "4 hr"). |
| appearance | "plain" | "outlined" | undefined | "plain" | plain draws the kit's text input, for a FormField; outlined draws the outlined field. |
| label | string | undefined | – | The field's name on the outline, with appearance="outlined"; omit it inside a FormField. |
| hint | ReactNode | – | Text under the outlined field; inside a FormField, use its hint. |
| error | string | undefined | – | What is wrong with the duration, from the caller; it replaces the field's own message. |
| required | boolean | undefined | – | Whether a duration must be given. |
| disabled | boolean | undefined | – | |
| readOnly | boolean | undefined | – | Shows the duration without a way to change it, and no step buttons or presets. |
| name | string | undefined | – | Submits the duration in unit with a form under this name; nothing while there is none. |
| id | string | undefined | – | The input's id, for a label elsewhere; inside a FormField, the field gives it. |
| placeholder | string | undefined | – | What the empty field shows, translated by the caller; a written example ("4 hr 30 min") unless set. |
| className | string | undefined | – | Classes added to the field's root. |
| testId | string | undefined | – | data-testid on the field; ${testId}--input on the input. |
| ref | Ref<HTMLInputElement> | undefined | – | The input element. |
| motion | MotionProp | undefined | – | How the outlined field's error line moves; see OutlinedField. |
Related
- Time input: a time of day, not a length of time.
- Number input: a number with a unit that is not a duration.
- Countdown: the time left until a moment, written the same way.