Use it for
Use BbTimePicker to set a recurring schedule time from your own cell, chip, or
button.
Use something else when
BbTimePickerInput: people must type the time or submit it as a labelled form fieldBbDatePicker: the time belongs to a specific calendar dateBbCalendar: the day and the time are picked in the page, from a calendar that stays open
Passthrough
Hover or tap a part to outline it. The toggles turn on loading, errors and warnings where the component has them; only parts get a row.
Set a schedule time
Use one picker per schedule value, such as each warehouse's dispatch time.
| Warehouse | Trucks leave at |
|---|---|
| Milan North | Dispatch time for Milan North |
| Berlin Spandau | Dispatch time for Berlin Spandau |
| Lyon Est | Dispatch time for Lyon Est |
<template>
<table class="w-full max-w-md text-sm">
<caption class="mb-2 text-left text-sm font-medium">
Daily dispatch
</caption>
<thead>
<tr class="text-left text-xs uppercase opacity-60">
<th class="py-1 font-medium" scope="col">Warehouse</th>
<th class="py-1 font-medium" scope="col">Trucks leave at</th>
</tr>
</thead>
<tbody>
<tr
v-for="site in sites"
:key="site.id"
class="border-t border-(--bb-border)"
>
<td class="py-2">{{ site.name }}</td>
<td class="py-2">
<!-- One picker per value; `v-bind="props"` is the whole wiring. -->
<BbTimePicker
v-model="site.dispatchAt"
:label="`Dispatch time for ${site.name}`"
:step="15"
>
<template #activator="{ props, value }">
<BbButton
prepend:icon="lucide:clock"
size="sm"
:variant="value ? 'outline' : 'ghost'"
v-bind="props"
>
{{ value ?? 'Set a time' }}
</BbButton>
</template>
</BbTimePicker>
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbButton, BbTimePicker } from 'bitboss-ui';
type Site = { id: string; name: string; dispatchAt: string | null };
// `null` is "no time yet"; otherwise a 24-hour `HH:mm` string.
const sites = ref<Site[]>([
{ id: 'mil', name: 'Milan North', dispatchAt: '07:30' },
{ id: 'ber', name: 'Berlin Spandau', dispatchAt: '08:00' },
{ id: 'lyo', name: 'Lyon Est', dispatchAt: null },
]);
</script>
The model is null or a zero-padded 24-hour string such as 09:00 (09:00:30
with seconds). It is a wall-clock value: it has no date or timezone and does
not shift between regions.
Bind the activator slot's props to your trigger: they carry the click handler,
the anchor, the accessible name and the open state. Give the picker a specific
label: the trigger is announced as that label followed by its own text
(“Dispatch time for Milan North 07:30”).
Display and granularity
Use ampm for a 12-hour display, seconds for HH:mm:ss, and step for the
minute rows offered by the columns.
- Meeting starts 12-hour, quarter-hour steps
- Meeting start
- Callback window 09:00–18:00 only
- Callback time
- Sensor reading To the second
- Measurement time
<template>
<dl
class="grid max-w-md grid-cols-[1fr_auto] items-center gap-x-4 gap-y-3 text-sm"
>
<dt>
Meeting starts
<span class="block text-xs opacity-60">12-hour, quarter-hour steps</span>
</dt>
<dd class="justify-self-end">
<BbTimePicker v-model="meeting" ampm label="Meeting start" :step="15">
<!-- `text` is the value as the picker shows it: "2:30 PM" under `ampm`. -->
<template #activator="{ props, text }">
<BbButton
prepend:icon="lucide:clock"
size="sm"
variant="outline"
v-bind="props"
>
{{ text || 'Set a time' }}
</BbButton>
</template>
</BbTimePicker>
</dd>
<dt>
Callback window
<span class="block text-xs opacity-60">
{{ fixedTimes.opensAt }}–{{ fixedTimes.closesAt }} only
</span>
</dt>
<dd class="justify-self-end">
<BbTimePicker
v-model="callback"
label="Callback time"
:max="fixedTimes.closesAt"
:min="fixedTimes.opensAt"
:step="30"
>
<template #activator="{ props, text }">
<BbButton
prepend:icon="lucide:phone"
size="sm"
variant="outline"
v-bind="props"
>
{{ text || 'Pick a time' }}
</BbButton>
</template>
</BbTimePicker>
</dd>
<dt>
Sensor reading
<span class="block text-xs opacity-60">To the second</span>
</dt>
<dd class="justify-self-end">
<BbTimePicker v-model="measured" label="Measurement time" seconds>
<template #activator="{ props, text }">
<BbButton
prepend:icon="lucide:gauge"
size="sm"
variant="outline"
v-bind="props"
>
{{ text || '—' }}
</BbButton>
</template>
</BbTimePicker>
</dd>
</dl>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbButton, BbTimePicker } from 'bitboss-ui';
import { fixedTimes } from '~/demo-data';
const meeting = ref<string | null>(fixedTimes.slot);
// Hours outside `min`/`max` stay visible in the columns, disabled.
const callback = ref<string | null>('10:00');
// `ampm` shows 12-hour columns (01–12) and an AM/PM column, but the model
// stays 24-hour ("14:30"); the activator's `text` is the 12-hour label.
const measured = ref<string | null>(fixedTimes.precise);
</script>
These props do not change the value contract. ampm still emits 24-hour text,
and step constrains picks without rounding an existing off-grid value.
min and max take HH:mm and disable the entries outside the window, as the
calendar does with days. A pick that still lands outside, such as an hour beside
a minute the window rules out, is clamped to the nearest bound and fires
error.
Schedule windows
Use range for a shift or service window, with [] as the empty model.
| Shift | Hours | Length |
|---|---|---|
| Day | Day hours | 8 h |
| Night | Night hours | 8 h |
| Weekend cover | Weekend cover hours | — |
<template>
<table class="w-full max-w-md text-sm">
<caption class="mb-2 text-left text-sm font-medium">
Front desk rota
</caption>
<thead>
<tr class="text-left text-xs uppercase opacity-60">
<th class="py-1 font-medium" scope="col">Shift</th>
<th class="py-1 font-medium" scope="col">Hours</th>
<th class="py-1 text-right font-medium" scope="col">Length</th>
</tr>
</thead>
<tbody>
<tr
v-for="shift in shifts"
:key="shift.id"
class="border-t border-(--bb-border)"
>
<td class="py-2">{{ shift.name }}</td>
<td class="py-2">
<!-- The toggle at the top of the panel picks the end being edited. -->
<BbTimePicker
v-model="shift.hours"
:label="`${shift.name} hours`"
range
:step="30"
>
<template #activator="{ props }">
<BbButton
prepend:icon="lucide:clock"
size="sm"
:variant="shift.hours.length === 2 ? 'outline' : 'ghost'"
v-bind="props"
>
{{
shift.hours.length === 2 ? shift.hours.join('–') : 'Set hours'
}}
</BbButton>
</template>
</BbTimePicker>
</td>
<td class="py-2 text-right tabular-nums opacity-70">
{{ length(shift.hours) }}
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbButton, BbTimePicker } from 'bitboss-ui';
import { fixedTimes } from '~/demo-data';
type Shift = { id: string; name: string; hours: string[] };
// An array at every moment; `[]` is "not set". The pair is kept as picked,
// so a night shift can end the next morning.
const shifts = ref<Shift[]>([
{
id: 'day',
name: 'Day',
hours: [fixedTimes.shiftStart, fixedTimes.shiftEnd],
},
{ id: 'night', name: 'Night', hours: ['22:00', '06:00'] },
{ id: 'weekend', name: 'Weekend cover', hours: [] },
]);
const toMinutes = (time: string): number => {
const [hour, minute] = time.split(':').map(Number) as [number, number];
return hour * 60 + minute;
};
// An end before the start means the shift crosses midnight.
const length = (hours: string[]): string => {
if (hours.length !== 2) return '—';
const minutes = (toMinutes(hours[1]!) - toMinutes(hours[0]!) + 1440) % 1440;
return `${Math.floor(minutes / 60)} h${minutes % 60 ? ` ${minutes % 60} min` : ''}`;
};
</script>
The pair is kept as picked, so an end before the start is a window that crosses
midnight; check the order yourself if yours cannot. The panel toggle chooses
which end is being edited; bind v-model:active-segment only when another control
must read or change it.
Trigger and mobile surface
Put v-bb-time on a table cell to skip a wrapper per row, or pass a button you
already have to the activator prop.
| Day | Opens | Closes |
|---|---|---|
| Friday | ||
| Saturday | ||
| Sunday |
<template>
<table class="w-full max-w-md text-sm">
<caption class="mb-2 text-left font-medium">
Store hours
</caption>
<thead>
<tr class="text-left text-xs uppercase opacity-60">
<th class="py-1 font-medium" scope="col">Day</th>
<th class="py-1 font-medium" scope="col">Opens</th>
<th class="py-1 font-medium" scope="col">Closes</th>
</tr>
</thead>
<tbody>
<tr
v-for="day in week"
:key="day.id"
class="border-t border-(--bb-border)"
>
<td class="py-2">{{ day.name }}</td>
<td v-for="edge in edges" :key="edge.key" class="py-1">
<!-- No wrapper per cell: the directive takes the picker's props. -->
<BbButton
v-bb-time="{
modelValue: day[edge.key],
// Typed by mode: one time hands a string or null.
'onUpdate:modelValue': (time) => (day[edge.key] = time),
label: `${day.name} ${edge.label}`,
step: 15,
offCanvasProps: { title: `${day.name} ${edge.label}` },
}"
size="sm"
variant="ghost"
>
{{ day[edge.key] ?? 'Closed' }}
</BbButton>
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbButton } from 'bitboss-ui';
import { fixedTimes } from '~/demo-data';
type Day = {
id: string;
name: string;
opensAt: string | null;
closesAt: string | null;
};
const edges = [
{ key: 'opensAt', label: 'opening time' },
{ key: 'closesAt', label: 'closing time' },
] as const;
const week = ref<Day[]>([
{ id: 'fri', name: 'Friday', opensAt: fixedTimes.opensAt, closesAt: '20:00' },
{
id: 'sat',
name: 'Saturday',
opensAt: '10:00',
closesAt: fixedTimes.closesAt,
},
{ id: 'sun', name: 'Sunday', opensAt: null, closesAt: null },
]);
</script>
With a button you already have, drop the slot and pass its ref:
<script setup lang="ts">
import { ref } from 'vue';
import { BbButton, BbTimePicker } from 'bitboss-ui';
const time = ref<string | null>(null);
const trigger = ref();
</script>
<template>
<BbButton ref="trigger" variant="outline">Pick a time</BbButton>
<BbTimePicker v-model="time" :activator="trigger" />
</template>
The columns support arrow keys and digit type-ahead, so add no key handlers of
your own. On phones, adaptive opens them in a bottom sheet, titled here through
offCanvasProps; set :adaptive="false" for viewport-independent tests.
Desktop placement props do not apply to the sheet.