Use it for
Use BbDatePicker to assign a date from a trigger you own, such as a due-date
cell or booking chip.
Use something else when
BbDatePickerInput: people must type the value or submit it as a labelled form fieldBbTimePicker: the value is a recurring wall-clock time with no dateBbPopover: the panel contains something other than a calendar
Pass Through
Hover or tap a part to outline it. Toggles flip loading, errors and warnings when the component has them — only parts highlight.
Decide whether the model represents a calendar label or an instant before building the trigger.
Choose the value first
Use floating for due dates, birthdays, report filters, and other calendar days.
It emits YYYY-MM-DD, with no time or timezone to shift the day.
null<template>
<div class="flex items-center gap-3">
<BbDatePicker v-model="due" floating label="Due date for this task">
<!-- `v-bind="props"` is the whole wiring: reference, click and ARIA. -->
<template #activator="{ props, value }">
<button v-bind="props" class="cell" type="button">
{{ value ?? 'Set a due date' }}
</button>
</template>
</BbDatePicker>
<span class="text-sm opacity-70">
Model — <code>{{ due ?? 'null' }}</code>
</span>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbDatePicker } from 'bitboss-ui';
// `null` is the "no date yet" value. What an unset trigger looks like is up
// to your markup, because the markup is yours.
const due = ref<string | null>(null);
</script>
<style scoped>
.cell {
border: var(--bb-border-w) solid var(--bb-border);
border-radius: var(--bb-radius);
font-size: 0.875rem;
outline: none;
padding: 0.25rem 0.625rem;
}
.cell:focus-visible {
box-shadow:
0 0 0 2px var(--bb-panel),
0 0 0 4px var(--bb-ring);
}
</style>
Leave floating off when the value is an instant that different timezones may
display differently. The model then carries a local offset; add utc only when
the receiving API requires a Z timestamp. type="month" and type="year"
always emit calendar labels (YYYY-MM and YYYY).
Coming from v2datetime → type
The v3 alpha boolean datetime was removed. Use type="datetime" on both the
component and the v-bb-date directive.
Ranges and multiple dates
Use range for one continuous window. Keep its model as an array, including
[] for the empty state.
["2026-09-07","2026-09-11"]<template>
<div class="flex items-center gap-3">
<BbDatePicker
v-model="period"
floating
label="Reporting period"
:min="fixedDates.monthStart"
range
>
<template #activator="{ props, value }">
<button v-bind="props" class="chip" type="button">
{{
Array.isArray(value) && value.length === 2
? `${value[0]} → ${value[1]}`
: 'Pick a period'
}}
</button>
</template>
</BbDatePicker>
<span class="text-sm opacity-70">
<code>{{ JSON.stringify(period) }}</code>
</span>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbDatePicker } from 'bitboss-ui';
import { fixedDates } from '~/demo-data';
// An array at every moment, including the empty one: `[]`, never `null`.
const period = ref<string[]>([fixedDates.rangeStart, fixedDates.rangeEnd]);
</script>
<style scoped>
.chip {
border: var(--bb-border-w) solid var(--bb-border);
border-radius: var(--bb-radius);
font-size: 0.875rem;
outline: none;
padding: 0.25rem 0.625rem;
}
.chip:focus-visible {
box-shadow:
0 0 0 2px var(--bb-panel),
0 0 0 4px var(--bb-ring);
}
</style>
Use multiple for a set of independent days, months, or years.
2026-09-07, 2026-09-21, 2026-10-12<template>
<div class="flex items-center gap-3">
<!-- The panel stays open while the set is built: the next pick is the
point. Picking a selected cell removes it. -->
<BbDatePicker
v-model="closures"
floating
label="Office closures"
multiple
>
<template #activator="{ props, value }">
<button v-bind="props" class="chip" type="button">
{{ Array.isArray(value) ? value.length : 0 }} closure{{
Array.isArray(value) && value.length === 1 ? '' : 's'
}}
</button>
</template>
</BbDatePicker>
<span class="text-sm opacity-70">
<code>{{ closures.length ? closures.join(', ') : '[]' }}</code>
</span>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbDatePicker } from 'bitboss-ui';
import { closedDays } from '~/demo-data';
// The model is a sorted array the component maintains. `multiple` refuses to
// combine with `range` or `type="datetime"` — both throw rather than guess.
const closures = ref<string[]>([...closedDays]);
</script>
<style scoped>
.chip {
border: var(--bb-border-w) solid var(--bb-border);
border-radius: var(--bb-radius);
font-size: 0.875rem;
outline: none;
padding: 0.25rem 0.625rem;
}
.chip:focus-visible {
box-shadow:
0 0 0 2px var(--bb-panel),
0 0 0 4px var(--bb-ring);
}
</style>
multiple cannot be combined with range or type="datetime"; both
combinations throw.
Date and time
Use type="datetime" when the date and time identify one scheduled occurrence.
v-model 2026-08-31 · v-model:time 09:30<template>
<div class="flex items-center gap-3">
<BbDatePicker
v-model="day"
v-model:time="time"
floating
label="When this job runs"
:step="15"
type="datetime"
>
<template #activator="{ props, value }">
<button v-bind="props" class="chip" type="button">
{{ value ? `${value} at ${time}` : 'Schedule this job' }}
</button>
</template>
</BbDatePicker>
<span class="text-sm opacity-70">
<code>v-model</code> {{ day ?? 'null' }} ·
<code>v-model:time</code> {{ time ?? 'null' }}
</span>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbDatePicker } from 'bitboss-ui';
import { fixedDates, fixedTimes } from '~/demo-data';
// `floating` keeps the date a plain YYYY-MM-DD, so the time exists only in
// the second model. Drop `v-model:time` here and the time rail still works —
// and everything picked in it is thrown away.
const day = ref<string | null>(fixedDates.tomorrow);
const time = ref<string | null>(fixedTimes.standup);
</script>
<style scoped>
.chip {
border: var(--bb-border-w) solid var(--bb-border);
border-radius: var(--bb-radius);
font-size: 0.875rem;
outline: none;
padding: 0.25rem 0.625rem;
}
.chip:focus-visible {
box-shadow:
0 0 0 2px var(--bb-panel),
0 0 0 4px var(--bb-ring);
}
</style>
With floating, bind v-model:time: it is the only place the time is stored.
Without floating, the time is already part of the instant. ampm changes only
the display; the time model remains 24-hour.
Available and marked days
Set a continuous window with min and max, then use selectable for rules
such as weekdays or stock availability.
<template>
<div class="flex items-center gap-3">
<BbDatePicker
v-model="booking"
:first-day-of-week="1"
floating
label="Booking date"
:max="fixedDates.windowEnd"
:min="fixedDates.windowStart"
:selectable="isBookable"
>
<template #activator="{ props, value }">
<button v-bind="props" class="chip" type="button">
{{ value ?? 'Pick a day' }}
</button>
</template>
<!-- `append:day` ADDS below the number; `day` would REPLACE it.
Keep decorations cheap: this runs for all 42 visible cells. -->
<template #append:day="{ item }">
<span v-if="eventOn(item)" class="dot" />
</template>
</BbDatePicker>
<span class="text-sm opacity-70">
{{ booking ? (eventLabel ?? 'Nothing booked that day') : 'No day picked' }}
</span>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { BbDatePicker } from 'bitboss-ui';
import { closedDays, dayEvents, fixedDates } from '~/demo-data';
const booking = ref<string | null>(null);
/** The slot hands you the cell's dayjs object; this is all we need of it. */
type DayItem = { format: (template: string) => string };
const eventOn = (item: DayItem): boolean =>
dayEvents[item.format('YYYY-MM-DD')] !== undefined;
const eventLabel = computed(() =>
booking.value ? dayEvents[booking.value] : undefined
);
// Gate with `selectable`, never with a click handler on top of it: a disabled
// day is already announced as disabled, and a parallel handler desyncs them.
const isBookable = (day: string): boolean => !closedDays.includes(day);
</script>
<style scoped>
.chip {
border: var(--bb-border-w) solid var(--bb-border);
border-radius: var(--bb-radius);
font-size: 0.875rem;
outline: none;
padding: 0.25rem 0.625rem;
}
.chip:focus-visible {
box-shadow:
0 0 0 2px var(--bb-panel),
0 0 0 4px var(--bb-ring);
}
.dot {
background: currentcolor;
border-radius: 999px;
display: block;
height: 0.25rem;
margin: 0.125rem auto 0;
opacity: 0.7;
width: 0.25rem;
}
</style>
Use append:day for a lightweight marker. Use day only when you need to
replace the day number. Do not add a parallel click guard: rejected dates are
already disabled and announced as such.
Coming from v2Per-date slot names
v2 slot names used raw dates such as #2024-03-15. Replace hyphens with
underscores: #2024_03_15.
Trigger and mobile surface
Spread the activator slot's props onto the clickable element. They provide the
click handler, anchor, accessible label, and open state.
Give the picker a specific label, such as “Due date for task”. Focus enters the
calendar on open and returns to the trigger on close. The grid already supports
arrow keys, Home, End, Page Up, Page Down, Enter, and Space.
On mobile, adaptive opens the calendar in a bottom sheet. Pass sheet options
through offCanvasProps; set :adaptive="false" for viewport-independent
tests. Use the activator prop for an existing element, or v-bb-date for a
bare table cell that does not need slot state.
Coming from v2The mobile sheet is on by default
v3 inherits adaptive from the plugin, where it defaults to true. Set
:adaptive="false" to retain a popover on every viewport.