Time picker
A themed time field with hour and minute columns, holding a plain HH:mm string.
<script setup lang="ts">
import SaTimePicker from '@shared/components/common/SaTimePicker.vue'
import { ref } from 'vue'
const time = ref('09:30')
</script>
<template>
<div class="max-w-xs space-y-3">
<SaTimePicker v-model="time" />
<p class="text-sm text-muted-foreground">
Model value: {{ time || 'empty' }}
</p>
</div>
</template>Usage
v-model is a string, HH:mm on a 24-hour clock, and an empty string means no time. That stays true whatever the reader's language: only the columns and the trigger label change.
<input type="time"> is drawn differently by every browser, and none of them take the theme, the radius or the app's language. This control is columns of buttons instead. They are buttons rather than selects on purpose, because a select sends its menu to <body>, which the popover would count as a click outside itself and close on.
When the app's language writes a 12-hour clock, the columns show 1–12 with an AM/PM column beside them. Where it writes a 24-hour clock, such as German, the hours run 0–23 and the third column is not rendered. The model value does not change either way.
Examples
Minute steps
minuteStep thins the minute column. The default is 1, every minute, like a native time field. Whatever minute the value already holds is always offered, so a stored 09:32 still has a row to highlight under a 15-minute step.
<script setup lang="ts">
import SaTimePicker from '@shared/components/common/SaTimePicker.vue'
import { ref } from 'vue'
const slot = ref('14:15')
</script>
<template>
<div class="max-w-xs">
<SaTimePicker v-model="slot" :minute-step="15" placeholder="Pick a slot" />
</div>
</template>Empty
An empty string shows the placeholder. Press PM on an empty field and the hour column swings to the afternoon, so the press has a visible effect before a time exists.
<script setup lang="ts">
import SaTimePicker from '@shared/components/common/SaTimePicker.vue'
import { ref } from 'vue'
const reminder = ref('')
</script>
<template>
<div class="max-w-xs">
<SaTimePicker v-model="reminder" placeholder="No reminder" />
</div>
</template>Inside another field
bare drops the trigger's own border and shadow, for putting the control inside a box you draw yourself.
<script setup lang="ts">
import SaTimePicker from '@shared/components/common/SaTimePicker.vue'
import { ref } from 'vue'
const from = ref('09:00')
const to = ref('17:30')
</script>
<template>
<div class="flex max-w-sm items-center rounded-md border bg-background">
<SaTimePicker v-model="from" bare />
<span class="px-1 text-sm text-muted-foreground">to</span>
<SaTimePicker v-model="to" bare />
</div>
</template>Disabled
<script setup lang="ts">
import SaTimePicker from '@shared/components/common/SaTimePicker.vue'
import { ref } from 'vue'
const closing = ref('18:00')
</script>
<template>
<div class="max-w-xs">
<SaTimePicker v-model="closing" disabled />
</div>
</template>API
Source: src/shared/components/common/SaTimePicker.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | required | HH:mm on a 24-hour clock. An empty string means no time. |
id | string | — | Put on the trigger, so a <Label for> points at it. |
placeholder | string | 'common.pickATime' | The trigger text while the value is empty. The default renders as "Pick a time". |
disabled | boolean | false | Blocks the trigger. |
invalid | boolean | false | Sets aria-invalid on the trigger. |
minuteStep | number | 1 | The step between offered minutes. The value's own minute is always offered too. |
bare | boolean | false | Removes the trigger's border and shadow. |
Emits
| Event | Payload | Description |
|---|---|---|
update:modelValue | value: string | Fires on every hour, minute or AM/PM press, as HH:mm. |
Slots
None.
Accessibility
- Each column is a
listbox, named "Hour", "Minute" and "AM or PM", and every cell is anoptioncarryingaria-selected. - Opening the popover scrolls each column to its selected row rather than to midnight.
- The trigger takes
aria-invalidfrominvalid. Give it anidand a<Label for>, or anaria-label, so the field has a name.