Source: https://monarchui.dev/components/duration-input/

Components · Forms

# DurationInput

Free-text duration field ("1h 30m", "90m", "1.5h") with a minutes value and arrow-key steps.

@rookcom/monarch-react/scheduling

Component

## Estimate

## Usage examples

### Limits, days and small

## Purpose

Enters a length of time the way people say it and stores whole minutes: estimates, time logged, meeting lengths, SLA targets. Import from `@rookcom/monarch-react/scheduling`.

### Use when

- A duration in minutes or hours: estimates, time tracking, buffers, response targets.

### Avoid when

- A time of day: use a time picker.
- A date range: use DateRangePicker.
- A handful of fixed lengths (15, 30, 60 minutes): use SegmentedControl or Select.

## Design principles

- Accept what people type: "90", "90m", "1h 30m", "1h30", "1.5h", "1,5h", "1:30", "2 hours", "1d 4h".
- Show it back one way ("1h 30m") so the value is unambiguous.
- Never save a guess: unreadable or out-of-range text stays, is flagged and explained.
- The value is always whole minutes; formatting and parsing are replaceable for other conventions.

## API reference

```tsx
import { DurationInput, formatDuration, parseDuration } from '@rookcom/monarch-react/scheduling';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `DurationInput` | component | client | `.mn-duration-input-` |
| `formatDuration` | utility | client | – |
| `parseDuration` | utility | client | – |

Renders `.mn-duration-input` wrapping the input and its message. The ref and native input props go to the input; `className` and `style` go to the wrapper.

Helpers: `parseDuration(text, { minutesPerDay })` returns minutes, `null` (empty) or `NaN`; `formatDuration(minutes, { largestUnit, minutesPerDay })`.

Generated from the TypeScript declarations of `packages/react/src/components/forms/duration-input`.

## Properties

`onValueChange` fires on blur, Enter and arrow steps, never on every keystroke.

### `DurationInput`

Free-text duration field with a minutes value. Reads "1h 30m", "90m", "1.5h" or "1:30", shows the value back as "1h 30m", and steps with the arrow keys. Unreadable or out-of-range text is kept, flagged and explained, never saved. The input is a `spinbutton` with a spoken value.

| Prop | Type | Description |
| --- | --- | --- |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `defaultValue` | `number \| null` |  |
| `disabled` | `boolean` |  |
| `format` | `((minutes: number) => string)` | Custom display format. Default `formatDuration`. |
| `id` | `string` |  |
| `invalid` | `boolean` |  |
| `largestUnit` | `"h" \| "d"` | Largest unit when formatting: `h` (default) or `d`. |
| `max` | `number` | Largest allowed value in minutes. |
| `messages` | `Partial<DurationInputMessages>` | Interface text, for translation. |
| `min` | `number` | Smallest allowed value in minutes. Default 0. |
| `minutesPerDay` | `number` | Minutes in a "d" when parsing and formatting. Default 1440. |
| `onValueChange` | `((minutes: number \| null) => void)` | Called with minutes (or `null` when cleared) on blur, Enter or an arrow-key step; never while typing. |
| `parse` | `((text: string) => number \| null)` | Custom parser: minutes, `null` for empty, or `NaN` when unreadable. Default `parseDuration`. |
| `size` | `"sm" \| "md" \| "lg"` |  |
| `step` | `number` | Minutes added or removed by ArrowUp / ArrowDown. Default 15. PageUp / PageDown step by 60. |
| `value` | `number \| null` | Duration in whole minutes (controlled). `null` is empty. |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `size` | `'sm'``'md'``'lg'` | Input height from `--mn-control-*`. |
| `largestUnit` | `'h'``'d'` | "26h" or "1d 2h". Use `minutesPerDay={480}` for 8-hour work days. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Unreadable | `aria-invalid="true"` | "Use a duration like 1h 30m, 90m or 1.5h." under the field; the text is kept. |
| Out of range | `aria-invalid="true"` | "Enter between 15m and 2h." from `min` and `max`. |
| Empty | – | The value is `null`; `aria-valuenow` is removed. |
| Disabled / read-only | `disabled, readonly` | Keys do nothing. |

## Interaction behaviour

- Typing never changes the value; blur or Enter reads the text, saves minutes and reformats it.
- Clearing the text saves `null`.
- Escape restores the last saved value and clears the message.
- Arrow steps snap to the step grid (50m + 15 → 1h) and clamp to `min` and `max`.

## Keyboard support

| Keys | Action |
| --- | --- |
| ArrowUp / ArrowDown | Adds or removes `step` minutes (default 15). |
| PageUp / PageDown | Moves to the next or previous whole hour. |
| Home / End | Sets `min` / `max` (End only with `max`). |
| Enter | Reads and saves the text. |
| Escape | Restores the saved value. |

## Accessibility

Role

spinbutton

Accessible name

A FieldLabel or

aria-label

.

- `aria-valuenow` is the minutes, `aria-valuetext` a spoken form ("1 hour 30 minutes"), `aria-valuemin` / `aria-valuemax` the limits.
- The error message is linked with `aria-describedby` and announced as an alert.
- Field descriptions and errors are wired as for Input.

## Responsive behaviour

- Fills its container; set a width on the Field. Numbers are tabular so values do not jitter.

## Theme support

- Input tokens; the message uses `--mn-color-danger-strong` in every theme.

## Do and don't

### Do

- Say in a FieldDescription which formats work when the field is new to people.
- Set `min` and `max` when the product has limits, so the message can say them.

### Don't

- Do not use a number input with a separate unit select for durations people type.
- Do not save on every keystroke; wait for `onValueChange`.

## Related components

- [Input](https://monarchui.dev/components/text-field/): Plain text and number input.
- [Field](https://monarchui.dev/components/form-field/): Label, help and error wiring.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Field](https://monarchui.dev/components/form-field/) |

Uses the `.mn-input` styles from text-field.

## Testing coverage

- Parses 13 input shapes, rejects unreadable text, honours minutesPerDay, formats h/m and days.
- Commits on blur with spoken value; unreadable text is kept, flagged, explained and Escape restores.
- Range message; arrow, page, Home and End steps with snapping and clamping.
- Clearing to null, controlled updates, custom format and read-only.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/duration-input; Cross-browser axe: /components/duration-input |

Tests: `packages/react/src/components/forms/duration-input`

## All example code

### Estimate

```tsx
import { useState } from 'react';
import { Field, FieldDescription, FieldLabel } from '@rookcom/monarch-react';
import { DurationInput } from '@rookcom/monarch-react/scheduling';

function EstimateDemo() {
  const [minutes, setMinutes] = useState<number | null>(90);
  return (
    <Field style={{ width: '100%', maxWidth: 240 }}>
      <FieldLabel>Estimate</FieldLabel>
      <DurationInput value={minutes} onValueChange={setMinutes} />
      <FieldDescription>Type 1h 30m, 90m or 1.5h. Arrow keys step by 15 minutes.</FieldDescription>
    </Field>
  );
}

<EstimateDemo />
```

### Limits, days and small

```tsx
import { DurationInput } from '@rookcom/monarch-react/scheduling';

<div style={{ display: 'grid', gap: 12, width: '100%', maxWidth: 240 }}>
  <DurationInput
    aria-label="Buffer"
    size="sm"
    min={5}
    max={120}
    step={5}
    defaultValue={10}
  />
  <DurationInput aria-label="Response target" largestUnit="d" defaultValue={1680} />
</div>
```
