@rozie-ui/date-picker-angular
v0.1.6
Published
Idiomatic Angular headless WAI-ARIA single-date calendar (month grid, prev/next nav, weekStartsOn, min/max/disabledDates, roving keyboard focus, ControlValueAccessor) — one accessible Rozie source compiled to Angular.
Maintainers
Readme
@rozie-ui/date-picker-angular
Idiomatic angular 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-angularPeer dependencies: @angular/core + @angular/common + @angular/forms. Install them alongside this package.
Also installed: @rozie/runtime-angular and @rozie/runtime-keynav-core — Rozie's small, tree-shaken runtime helper packages (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
import { Component } from '@angular/core';
import { DatePicker } from '@rozie-ui/date-picker-angular';
@Component({
selector: 'app-demo',
standalone: true,
imports: [DatePicker],
template: `
<DatePicker [(value)]="date" min="2026-01-01" (change)="onChange($event)" />
<!-- Range selection with quick-pick presets -->
<DatePicker
selectionMode="range"
[(value)]="range"
[presetRanges]="presetRanges"
(rangeComplete)="onRange($event)"
/>
`,
})
export class DemoComponent {
date = ''; // ISO YYYY-MM-DD, '' = no selection
range: { start: string; end: string } = { start: '', end: '' };
presetRanges = [
{ label: 'Q1 2026', range: { start: '2026-01-01', end: '2026-03-31' } },
{ label: 'Last 7 days', range: () => {
const iso = (d: Date) => d.toISOString().slice(0, 10);
return { start: iso(new Date(Date.now() - 6 * 864e5)), end: iso(new Date()) };
} },
];
onChange(e: { value: string }) {
console.log('picked:', e.value);
}
onRange(e: { value: { start: string; end: string } }) {
console.log('range:', e.value);
}
}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-angular/themes/shadcn.css'; // or material.css, bootstrap.css, base.cssAngular forms
The generated class implements ControlValueAccessor — the value model prop is the control value, so the date picker binds to template-driven and reactive forms directives directly, with no wrapper directive:
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormControl } from '@angular/forms';
import { DatePicker } from '@rozie-ui/date-picker-angular';
@Component({
selector: 'app-date-form',
standalone: true,
imports: [DatePicker, ReactiveFormsModule],
template: `
<!-- The selected ISO date IS the form control value -->
<DatePicker [formControl]="date" min="2026-01-01" />
`,
})
export class DateFormComponent {
date = new FormControl<string>('');
}
// Template-driven forms work the same way:
// <DatePicker [(ngModel)]="date" name="date" />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. |
@Component({ /* ... */ })
export class DemoComponent {
@ViewChild(DatePicker) picker!: DatePicker; // or the viewChild() signal
jumpToToday() { this.picker.goToToday(); }
reset() { this.picker.clear(); }
}Slots
| Slot | Params | | --- | --- | | header | label, prev, next, disabled, openMonths, openYears, closeDrill, viewMode | | footer | today, clear, todayIso | | presets | presets, apply |
