Use it for
Use BbTimePickerInput for a recurring time with no date, such as support hours,
a shift boundary, or a daily dispatch.
Use something else when
BbTimePicker: the time is assigned from your own cell, chip, or buttonBbDatePickerInputwithtype="datetime": the time belongs to a specific calendar date
Also see: Validated forms.
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 support hours
Give each opening and closing time its own labelled field; each submits a 24-hour value.
<template>
<fieldset class="flex max-w-lg flex-col gap-3">
<legend class="mb-2 text-sm font-medium">Support hours</legend>
<div
v-for="day in schedule"
:key="day.id"
class="grid grid-cols-[1fr_auto_1fr] items-center gap-x-3 gap-y-2 sm:grid-cols-[8rem_7rem_1rem_7rem]"
>
<BbSwitch
:id="`${day.id}-open`"
v-model="day.open"
class="col-span-3 sm:col-span-1"
:label="day.label"
:name="`${day.id}_open`"
/>
<!-- One field per value, each with its own id, name and label. -->
<BbTimePickerInput
:id="`${day.id}-opens`"
v-model="day.opensAt"
compact
:disabled="!day.open"
hide-label
:label="`${day.label} opens at`"
:name="`${day.id}_opens_at`"
/>
<span aria-hidden="true" class="text-center opacity-50">–</span>
<BbTimePickerInput
:id="`${day.id}-closes`"
v-model="day.closesAt"
compact
:disabled="!day.open"
hide-label
:label="`${day.label} closes at`"
:min="day.opensAt ?? undefined"
:name="`${day.id}_closes_at`"
/>
</div>
<p class="text-xs opacity-70">
Submitted: <code>{{ summary }}</code>
</p>
</fieldset>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { BbSwitch, BbTimePickerInput } from 'bitboss-ui';
import { fixedTimes } from '~/demo-data';
type OpeningHours = {
id: string;
label: string;
open: boolean;
opensAt: string | null;
closesAt: string | null;
};
// Wall clocks, always 24-hour and zero-padded. No date, no timezone: the one
// value in this family that is identical on every machine.
const schedule = ref<OpeningHours[]>([
{
id: 'weekdays',
label: 'Weekdays',
open: true,
opensAt: fixedTimes.opensAt,
closesAt: fixedTimes.closesAt,
},
{
id: 'saturday',
label: 'Saturday',
open: true,
opensAt: '10:00',
closesAt: '14:00',
},
{ id: 'sunday', label: 'Sunday', open: false, opensAt: null, closesAt: null },
]);
const summary = computed(() =>
schedule.value
.map(
(day) =>
`${day.id}: ${day.open ? `${day.opensAt}–${day.closesAt}` : 'closed'}`
)
.join(', ')
);
</script>
Pass a stable id and name; label does not supply the native name, and
inside a BbForm name is the field's key. The
separate hour and minute inputs auto-advance, accept pasted times, and support
arrow-key stepping.
A field joined to a BbForm by its name is required
through rules="required" or the schema. required
inside a form is a hint that marks the field and checks nothing, and the
asterisk follows a rule too (Validated forms).
Add formless to keep a
field that sits inside a form out of its data.
Wall-clock semantics
The model is null or a zero-padded 24-hour string such as 09:00. It contains
no date or timezone, so the same stored value means nine o'clock in every region.
- Model
"09:30"
<template>
<div class="flex max-w-sm flex-col gap-3">
<!-- `ampm`: the field shows the 12-hour form in the locale's words. -->
<BbTimePickerInput
id="standup-at"
v-model="standup"
ampm
clearable
label="Daily standup"
name="standup"
/>
<!-- The model never follows the display: it stays 24-hour. -->
<dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-sm">
<dt class="opacity-70">Model</dt>
<dd>
<code>{{ standup === null ? 'null' : `"${standup}"` }}</code>
</dd>
</dl>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbTimePickerInput } from 'bitboss-ui';
import { fixedTimes } from '~/demo-data';
const standup = ref<string | null>(fixedTimes.standup);
</script>
Do not parse the value with new Date('09:00'). Attach the relevant date before
doing instant or timezone arithmetic. With ampm the field displays the
12-hour form in the locale's words (09:30 AM), and the model stays 24-hour, as
the readout under the demo shows: there is nothing to format yourself.
Display and granularity
Use ampm for a 12-hour field, seconds for HH:mm:ss, and step for the
minute rows offered by the columns.
ampm + step 15 → 14:30 seconds → 14:30:45 step 15, value 10:07 — accepted, and no column row highlights for it <template>
<div class="flex max-w-sm flex-col gap-4">
<div class="flex flex-col gap-1">
<BbTimePickerInput
id="display-slot"
v-model="pickupSlot"
ampm
compact
label="Pickup slot"
name="pickup_slot"
:step="15"
/>
<code class="text-xs opacity-70"
>ampm + step 15 → {{ pickupSlot ?? 'null' }}</code
>
</div>
<div class="flex flex-col gap-1">
<BbTimePickerInput
id="display-precise"
v-model="precise"
compact
label="Measured at"
name="measured_at"
seconds
/>
<code class="text-xs opacity-70">
seconds → {{ precise ?? 'null' }}
</code>
</div>
<div class="flex flex-col gap-1">
<BbTimePickerInput
id="display-offgrid"
v-model="offGrid"
compact
label="Typed off the grid"
name="off_grid"
:step="15"
/>
<code class="text-xs opacity-70">
step 15, value {{ offGrid ?? 'null' }} — accepted, and no column row
highlights for it
</code>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbTimePickerInput } from 'bitboss-ui';
import { fixedTimes } from '~/demo-data';
// 12-hour fields; the model stays 24-hour.
const pickupSlot = ref<string | null>(fixedTimes.slot);
// With `seconds`, the model grows a third segment: HH:mm:ss.
const precise = ref<string | null>(fixedTimes.precise);
// `step` shapes the columns, not what typing accepts.
const offGrid = ref<string | null>(fixedTimes.offGrid);
</script>
step constrains column picks, not typing. An off-grid typed value remains
unchanged and highlights no row.
Bounds and ranges
Use min and max to constrain a callback window. The columns disable the times
outside it; a typed one is clamped on commit and reported through error.
Type 07:00 and leave the field: it snaps to 09:00 and says so.
<template>
<div class="flex max-w-sm flex-col gap-3">
<BbTimePickerInput
id="callback-at"
v-model="callAt"
clearable
:errors="message ? [message] : []"
hint="We call between 09:00 and 18:00."
label="Callback time"
:max="fixedTimes.closesAt"
:min="fixedTimes.opensAt"
name="call_at"
persistent-hint
:step="15"
@active="message = ''"
@error="onError"
/>
<p class="text-xs opacity-60">
Type <code>07:00</code> and leave the field: it snaps to
<code>{{ fixedTimes.opensAt }}</code> and says so.
</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbTimePickerInput } from 'bitboss-ui';
import type { TimePickerInputError } from 'bitboss-ui';
import { fixedTimes } from '~/demo-data';
// Seeded inside the window so the columns open on pickable times, with the
// disabled early hours just above as context.
const callAt = ref<string | null>('10:00');
// Cleared on `active` rather than on the next value change: the clamp that
// produced the message *is* a value change.
const message = ref('');
const onError = (error: TimePickerInputError) => {
if (error.code === 'disabled_time') {
message.value = `${error.original} is outside business hours, so it moved to ${error.normalized}.`;
}
};
</script>
Use range for a shift and keep the model as an array.
["08:00","16:00"]
Type 16:00 then 08:00 and leave the field: the pair swaps.
<template>
<div class="flex max-w-sm flex-col gap-3">
<BbTimePickerInput
id="shift"
v-model="shift"
clearable
label="Shift"
name="shift"
range
:step="30"
@active="swapped = ''"
@error="onError"
/>
<p class="text-sm opacity-70">
<template v-if="hours.length === 2">
<code>{{ JSON.stringify(hours) }}</code>
</template>
<template v-else> No shift yet. </template>
</p>
<p v-if="swapped" class="text-sm">{{ swapped }}</p>
<p class="text-xs opacity-60">
Type <code>16:00</code> then <code>08:00</code> and leave the field: the
pair swaps.
</p>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { BbTimePickerInput, type TimePickerInputError } from 'bitboss-ui';
import { fixedTimes } from '~/demo-data';
const shift = ref<string[] | null>([fixedTimes.shiftStart, fixedTimes.shiftEnd]);
const hours = computed(() => shift.value ?? []);
// Cleared on `active`: the swap that set it is itself a model update.
const swapped = ref('');
function onError(error: TimePickerInputError) {
if (error.code === 'end_before_start') {
swapped.value = `The end came before the start, so the shift now runs ${error.normalized.join('–')}.`;
}
}
</script>
Seed and clear it with []. A reversed range is swapped on commit
and emits end_before_start, so the field cannot hold a window that crosses
midnight: use BbTimePicker for a night shift. Turn error
payloads into application messages through errors; clear them on active.
Typing and mobile
Use disable-writing="mobile" when touch users should choose from the columns
while desktop users keep keyboard entry.
<template>
<div class="flex max-w-sm flex-col gap-4">
<!-- Typing off on phones, where the columns are easier than six taps
into a segmented field; typing on wherever there is a keyboard. -->
<BbTimePickerInput
id="platform-slot"
v-model="slotAt"
description="Type it here, use the columns on a phone."
disable-writing="mobile"
label="Delivery window opens"
name="slot_at"
:off-canvas-props="{ title: 'Pick a time' }"
:step="30"
/>
<!-- A floating popover on every viewport, phone included. -->
<BbTimePickerInput
id="platform-cutoff"
v-model="cutoffAt"
:adaptive="false"
description="A popover on every screen size."
label="Order cut-off"
name="cutoff_at"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbTimePickerInput } from 'bitboss-ui';
import { fixedTimes } from '~/demo-data';
const slotAt = ref<string | null>(fixedTimes.opensAt);
const cutoffAt = ref<string | null>(fixedTimes.closesAt);
</script>
adaptive opens the columns in a bottom sheet on mobile. Pass sheet options
through offCanvasProps, or set :adaptive="false" for viewport-independent
tests. Shared label, message, state, and form props follow
BbTextInput.
variant="none" drops every colour, focus ring included, for a field you style yourself, like the flat search field on BbTextInput.
Coming from v2has-warning → has-warnings
The shared v2 input prop hasWarning is now hasWarnings.