Skip to content

Behaviour ​

Typing and committing ​

  • Valid input updates v-model while you type. Invalid input leaves it unchanged.
  • Clearing the field sets v-model to null. This also happens with required, which additionally reports an empty error.
  • On blur and Enter, valid text is rewritten to a normalized form: 189h becomes 7d 21h.
  • If v-model changes from outside, the text is reformatted.

When errors appear ​

When an error is shown:

  • The input gets aria-invalid="true" and data-error="<code>".
  • The root gets data-invalid.
  • A message appears below the field, linked through aria-describedby.
  • The error event fires with the code, and again with null once the error clears.

validateOn controls when that happens:

validateOnBehaviour
'eager' (default)Not while you're still typing. The error appears on blur or Enter. While it's shown, it updates live, so fixing the input clears it right away.
'blur'The shown error only changes on blur or Enter. Stepping, reverting and outside changes to v-model also clear it.
'input'The error updates on every keystroke.

Whatever the mode, calling the exposed validate() shows the current error, e.g. on submit.

v-model: null

Preview ​

With preview, the normalized value appears inside the field as you type (= 1h 30min), but only when it differs from what you typed. It's also announced to screen readers through a polite live region, after a 600 ms pause in typing so not every keystroke is read out.

v-model: null

preview only works with the built-in field. With as, use the preview slot prop of the renderless slot instead.

Custom preview ​

formatPreview renders the preview yourself. It gets the duration in seconds and a context with the default preview (normalized), the typed text, the model value and the display locale:

vue
<DurationInput
  v-model="minutes"
  preview
  :format-preview="(seconds, { value }) => `≈ ${(seconds / 3600).toFixed(1)} hours (${value} min)`"
/>
v-model: null

Its output is shown and announced as is, without the leading = . The preview still only appears while the text differs from its normalized form, so it goes away on blur. Return null (or an empty string) to hide it for a value.

formatPreview changes the preview slot prop and the preview of useDurationInput too. It only affects the preview: the text is still normalized with displayStyle and displayUnits on blur.

Keyboard ​

KeyAction
↑ / ↓Step by step (default 15min), snapping to multiples: 1h07 → 1h15 / 1h
Shift + ↑ / ↓Step by 1 hour, snapping to whole hours
PgUp / PgDnStep by 1 day (as configured in customUnits), snapping to whole days
EnterNormalize and show errors. The form still submits, unless a preset is highlighted in the open menu, in which case that preset is picked
EscClose the presets menu, or revert to the last committed value. When reverting, the event only stops propagating if something was reverted, so closing a dialog still works
Alt + ↓Open the presets menu

Details:

  • Steps stay within min/max and never go below zero.
  • If the text is invalid, stepping starts from the last valid value. From an empty field, ↑ gives one step and ↓ gives zero.
  • :step="false" turns all stepping off, including Shift and PgUp/PgDn.
  • Keys pressed with Ctrl, Cmd or Alt are left alone (apart from Alt+↓), so browser shortcuts keep working.
v-model: 67

Snapping and clamping ​

  • snapToStep rounds to the nearest step on blur and Enter, then keeps the result within min/max.
  • clamp keeps values within min/max instead of reporting out_of_range. It applies while typing and on commit.
v-model: null

Presets ​

presets shows suggestions in a menu below the field. The input is an ARIA combobox.

  • Opens on focus or click while the field is empty, on Alt+↓, and while typing, filtered by label.
  • While it's open: the arrow keys move through the options, Enter picks the highlighted one, and Esc closes it. Tab and blur close it too.
vue
<DurationInput v-model="minutes" :presets="['15m', '30m', '1h', { label: 'Half a day', value: '4h' }]" />
v-model: null

Each entry is one of:

  • duration text,
  • a number in the model's unit,
  • { label, value }.

Entries that can't be parsed are dropped. Style the menu through ui.menu and ui.option; the highlighted option has data-active.

The built-in menu is not rendered with as. To show presets with your own component, use the renderless slot, which gets presets and selectPreset.

Released under the MIT License.