@rozie-ui/date-picker-vue

Idiomatic Vue headless WAI-ARIA single-date calendar (month grid, prev/next nav, weekStartsOn, min/max/disabledDates, roving keyboard focus) — one accessible Rozie source compiled to Vue.
1,608 1
Install
npm install @rozie-ui/date-picker-vue
Latest Version:0.1.11
Vue:3
License:MIT
Last Updated:Oct 15, 2026
Links: GitHub  ·  npm
Maintainer: serpentblade

@rozie-ui/date-picker-vue

Idiomatic vue DatePicker — a headless, fully-accessible (WAI-ARIA) single-date calendar (a month grid with leading/trailing spill, prev/next month navigation, weekStartsOn rotation, min/max/disabledDates gating, roving keyboard focus, and localized Intl labels) compiled from one Rozie source. It is HEADLESS: accept the default token-themed calendar, or override the month-nav header via the scoped slot. Every visual value is a CSS custom property, so it re-skins to any design system. This package is generated; do not edit src/ by hand.

Install

npm i @rozie-ui/date-picker-vue

Peer dependencies: vue. Install them alongside this package.

Also installed: @rozie/runtime-vue — Rozie's small, tree-shaken runtime helper package (controllable state, keyboard navigation, event modifiers, and safe interpolation). It arrives as a regular dependency, so npm pulls it for you. Your bundler keeps only the helpers this component actually uses — typically a few hundred bytes to a few KB, minified and gzipped. What's in it and what it costs.

Usage

<script setup lang="ts">
import { ref } from 'vue';
import DatePicker from '@rozie-ui/date-picker-vue';

const date = ref(''); // ISO YYYY-MM-DD, '' = no selection
function onChange(e: { value: string }) {
  console.log('picked:', e.value);
}

// Range mode: value becomes a { start, end } object.
const range = ref({ start: '', end: '' });
const iso = (d: Date) => d.toISOString().slice(0, 10);
const presetRanges = [
  { label: 'Q1 2026', range: { start: '2026-01-01', end: '2026-03-31' } },
  // a () => { start, end } thunk is resolved fresh on render
  { label: 'Last 7 days', range: () => ({ start: iso(new Date(Date.now() - 6 * 864e5)), end: iso(new Date()) }) },
];
</script>

<template>
  <DatePicker v-model:value="date" min="2026-01-01" @change="onChange" />

  <!-- Custom header via the scoped #header slot -->
  <DatePicker v-model:value="date">
    <template #header="{ label, prev, next }">
      <div class="my-header">
        <button @click="prev">Prev</button>
        <strong>{{ label }}</strong>
        <button @click="next">Next</button>
      </div>
    </template>
  </DatePicker>

  <!-- Range selection with quick-pick presets + a #presets override -->
  <DatePicker
    selectionMode="range"
    v-model:value="range"
    :presetRanges="presetRanges"
    @rangeComplete="(e) => console.log('range:', e.value)"
  >
    <template #presets="{ presets, apply }">
      <button v-for="p in presets" :key="p.label" @click="apply(p.range)">{{ p.label }}</button>
    </template>
  </DatePicker>

  <!-- showFooter renders a default Today / Clear row; #footer overrides it -->
  <DatePicker v-model:value="date" show-footer />
</template>

Theming

Every visual value is a --rozie-datepicker-* CSS custom property — override any of them at any ancestor scope. Ready-made design-system bridges ship in the package:

import '@rozie-ui/date-picker-vue/themes/shadcn.css';    // or material.css, bootstrap.css, base.css

Props

Name Type Default Two-way (model) Required Description
value any '' ✓ The selected value (two-way r-model). Polymorphic on selectionMode: in single mode an ISO YYYY-MM-DD string ("" = nothing selected); in range mode a { start, end } object of ISO endpoints ("" = an unset endpoint). As the sole model: true prop it drives the Angular ControlValueAccessor, so a DatePicker is a form control ([(ngModel)] / [formControl] bind directly). Selecting a day writes the new value back and emits change. Lit caveat (range mode): the object form must be delivered via a property binding (.value=${obj} / r-model), never a string value="..." attribute — the same rule already in force for disabledDates.
selectionMode String "single" Selection mode: 'single' (the default — value is one ISO YYYY-MM-DD string, fully backward-compatible) or 'range' (value becomes a { start, end } object selected with two clicks plus a live hover preview, direction-agnostic). In range mode a completed selection additionally emits rangeComplete.
min String null Inclusive lower bound as an ISO YYYY-MM-DD string. Days before it are rendered disabled and cannot be selected or focused. null (the default) imposes no lower bound.
max String null Inclusive upper bound as an ISO YYYY-MM-DD string. Days after it are rendered disabled and cannot be selected or focused. null (the default) imposes no upper bound.
disabledDates Array [] An array of ISO YYYY-MM-DD strings to disable individually (e.g. holidays or already-booked days), in addition to the min/max bounds. Disabled days are non-interactive and marked aria-disabled.
weekStartsOn Number 0 The first day of the week as a number, 0 = Sunday through 6 = Saturday. Rotates both the weekday header row and the grid columns (e.g. 1 for a Monday-first calendar).
disabled Boolean false Disable the entire control — every day cell and the previous/next month buttons become non-interactive and are marked aria-disabled. Also sets the Angular ControlValueAccessor disabled state.
locale String "en-US" BCP-47 locale tag used by Intl.DateTimeFormat to render the month-year heading and the short weekday header labels (e.g. "fr-FR", "ja-JP"). Falls back to English names in a runtime without Intl.
labels Object {} Optional overrides for the 10 static English "chrome" strings, keyed by root, previousMonth, nextMonth, changeMonthYear, changeYear, chooseMonth, chooseYear, presets, today, clear (defaults: "Date picker", "Previous month", "Next month", "Change month and year", "Change year", "Choose month", "Choose year", "Date range presets", "Today", "Clear"). Honest split: Intl is a date/number formatter, not a message catalog — it can localize a DATE but cannot translate the phrase "Previous month". The day-cell accessible name, each multi-month panel's own grid caption, the weekday header long names, and the month-year heading text are already Intl-derived from the locale prop and are NOT labels keys; the 10 chrome phrases above are English-static and only labels can translate them. An empty object (the default) yields the English defaults with zero config. Lit caveat: pass via a property binding (.labels=${…}), never a string attribute — the same rule already in force for disabledDates/presetRanges.
presetRanges Array [] Quick-pick presets for range mode — an array of { label, range } where range is a literal { start, end } value or a () => { start, end } thunk (the consumer owns the date math and i18n labels). Renders a default preset rail beneath the grid; the #presets slot overrides it. Lit caveat: pass via a property binding (.presetRanges=${[…]}) — thunks inside the array cannot survive a string attribute, same as disabledDates.
monthYearNav Boolean true Render the month-year heading as a clickable drill button that navigates days → months → years (and a year label that drills months → years). Capability-on: this is the documented exception to the boolean-default-false rule — the drill navigation is the ergonomic win of this feature, so it defaults to true. Set :month-year-nav="false" to restore the static heading <span> (byte-identical to the pre-navigation output).
numberOfMonths Number 1 How many month grids to render side by side, anchored at the view month and stepping forward (e.g. 2 for a two-up range calendar). 1 (the default) emits exactly the single-month markup with no extra wrapper element.
showFooter Boolean false Render a Today / Clear footer row beneath the calendar grid. Today selects (single mode) or navigates to (range mode) the current date; Clear deselects. The #footer slot fully overrides the default row, receiving { today, clear, todayIso }.
disabledDaysOfWeek Array [] An array of weekday indices to disable, Number[] where 0 = Sunday through 6 = Saturday (e.g. [0, 6] disables every weekend). Serializable, so it passes fine as a plain attribute. Threaded through the single gating funnel, so disabled weekdays are non-interactive, non-focusable, and marked aria-disabled — in agreement with day cells, drill enablement, and keyboard focus.
isDateDisabled Function null A consumer predicate (iso: string) => boolean — return true to disable the given ISO YYYY-MM-DD date (e.g. custom holiday / blackout rules beyond disabledDates/min/max). Threaded through the single gating funnel so day cells, drill enablement, and focus all agree. Lit caveat: pass via a property binding (.isDateDisabled=${fn}), never a string attribute — a function cannot survive attribute serialization, the same rule already in force for disabledDates/presetRanges.

Events

Event Description
change Fired whenever the selected value changes — selecting a day, applying a preset, or a programmatic clear(). Payload { value } — the new value: in single mode the selected ISO YYYY-MM-DD string (or "" when cleared); in range mode the { start, end } object (an in-progress anchor is { start, end: "" }; cleared is { start: "", end: "" }). Not fired when the picked date equals the current selection.
rangeComplete Range mode only. Fired when a range selection completes — the second endpoint lands (the two-click commit) or a preset is applied. Payload { value } — the ordered { start, end } object (start <= end). NOT fired on the first (anchor-only) click. Per-target consumer prop casing differs: React onRangeComplete, Vue @range-complete, Svelte onrangecomplete (lowercased), Angular (rangeComplete), Solid onRangeComplete, Lit range-complete (addEventListener — kebab-cased at dispatch, NOT case-preserved).

Imperative handle

Beyond props, the component exposes imperative methods (declared once in the Rozie source via $expose). Grab a handle with the native ref mechanism and call them directly:

Method Description
focus Move keyboard focus into the calendar grid — onto the selected day, else today (when in view), else the first visible day. Useful right after the picker becomes visible.
goToToday Swing the displayed month to today's month without changing the selection (a view-only navigation).
clear Deselect the current date (sets value to ""). Emits change with an empty value unless nothing is selected.
<script setup>
import { ref } from 'vue';
const picker = ref();          // template ref
</script>

<template>
  <DatePicker ref="picker" v-model:value="date" />
  <button @click="picker.goToToday()">Today</button>
</template>

Slots

Slot Params
header label, prev, next, disabled, openMonths, openYears, closeDrill, viewMode
footer today, clear, todayIso
presets presets, apply

Related Packages

@rozie-ui/combobox-vue

Idiomatic Vue headless WAI-ARIA combobox / autocomplete (text input + popup list...

2,591 1
@rozie-ui/toast-vue

Idiomatic Vue headless, accessible toast / notification host (queue, auto-dismis...

1,675 1
@rozie-ui/data-table-vue

Idiomatic Vue headless WAI-ARIA data table (sorting, filtering, pagination, sele...

3,040 1
@rozie-ui/otp-vue

Idiomatic Vue headless WAI-ARIA one-time-code / PIN input (segmented cells, past...

1,472 1

Version History

Version Released Vue License
0.1.11 ^3.4 MIT
0.1.10 ^3.4 MIT
0.1.9 ^3.4 MIT
0.1.8 ^3.4 MIT
0.1.7 ^3.4 MIT