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
Pass Through
Hover or tap a part to outline it. Toggles flip loading, errors and warnings when the component has them — only parts highlight.
BbTimePickerInput is new in v3.
Set support hours
Start with the schedule task: bind a labelled opening time and submit its 24-hour value.
Model: "09:00"
<template>
<div class="flex max-w-sm flex-col gap-3">
<BbTimePickerInput
id="support-opens-at"
v-model="supportOpensAt"
description="Used for the recurring weekday schedule."
label="Support opens at"
name="support_opens_at"
prepend:icon="lucide:clock"
/>
<p class="text-sm opacity-70">
Model:
<code>{{
supportOpensAt === null ? 'null' : `"${supportOpensAt}"`
}}</code>
</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbTimePickerInput } from 'bitboss-ui';
import { fixedTimes } from '~/demo-data';
// A wall clock, always 24-hour and zero-padded. No date, no timezone — the
// one value in this family that is identical on every machine.
const supportOpensAt = ref<string | null>(fixedTimes.opensAt);
</script>
Pass a stable id and name; label does not supply the native name. The
separate hour and minute inputs auto-advance, accept pasted times, and support
arrow-key stepping.
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"- Shown as
- 9:30 AM
<template>
<div class="flex max-w-sm flex-col gap-3">
<BbTimePickerInput
id="standup-at"
v-model="standup"
ampm
clearable
label="Daily standup"
name="standup"
/>
<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>
<dt class="opacity-70">Shown as</dt>
<dd>{{ twelveHour }}</dd>
</dl>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { BbTimePickerInput } from 'bitboss-ui';
import { fixedTimes } from '~/demo-data';
const standup = ref<string | null>(fixedTimes.standup);
// The model is never 12-hour, whatever `ampm` shows. Render your own display
// copy from the 24-hour string — and never with `new Date('09:30')`, which is
// not a date at all.
const twelveHour = computed(() => {
if (!standup.value) return '—';
const [rawHour, minute] = standup.value.split(':');
const hour = Number(rawHour);
const meridiem = hour < 12 ? 'AM' : 'PM';
return `${hour % 12 === 0 ? 12 : hour % 12}:${minute} ${meridiem}`;
});
</script>
Do not parse the value with new Date('09:00'). Attach the relevant date before
doing instant or timezone arithmetic. The model remains 24-hour even when the
field displays AM/PM.
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. A typed value outside the
window 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
@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';
const callAt = ref<string | null>(null);
// 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) => {
message.value =
error.code === 'disabled_time'
? `${error.original} is outside business hours — moved to ${error.normalized}.`
: 'The end came before the start, so the two were swapped.';
};
</script>
Use range for a shift. Keep the model as an array and use [] for empty.
["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"
prepend:icon="lucide:clock"
range
:step="30"
/>
<p class="text-sm opacity-70">
<template v-if="shift.length === 2">
<code>{{ JSON.stringify(shift) }}</code>
</template>
<template v-else>
No shift yet — the model is <code>[]</code>, never <code>null</code>.
</template>
</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 { ref } from 'vue';
import { BbTimePickerInput } from 'bitboss-ui';
import { fixedTimes } from '~/demo-data';
// Range mode needs an array at every moment, the empty one included.
const shift = ref<string[]>([fixedTimes.shiftStart, fixedTimes.shiftEnd]);
</script>
A reversed range is swapped on commit and emits end_before_start. 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"
label="Order cut-off (no mobile sheet)"
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 validated-field props follow
BbTextInput.
Coming from v2has-warning → has-warnings
The shared v2 input prop hasWarning is now hasWarnings.