Date Picker

Free

Custom calendar picker with month/year navigation, today indicator, min/max constraints and day/month/year views.

</>sv-date-picker datecalendarinput
import { SvDatePickerComponent } from '@styloviz/input-field';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Required with bounds

example.htmlAngular
<sv-date-picker
  label="Start date"
  [required]="true"
  minDate="2025-01-01"
  [(value)]="start" />

Accessible name + open/close events

ariaLabel names the trigger; opened/closed fire on popover state. Value is a YYYY-MM-DD string.

example.htmlAngular
<sv-date-picker
  ariaLabel="Start date"
  [(value)]="start"
  (opened)="onOpen()"
  (closed)="onClose()" />

Reactive forms (formControlName)

Implements ControlValueAccessor, so it binds to reactive and template-driven forms. The control value is a YYYY-MM-DD string; formControl.disable() disables the trigger.

example.htmlAngular
// form = new FormGroup({ start: new FormControl('2025-01-15') });
<sv-date-picker formControlName="start" label="Start date" />

<!-- or template-driven -->
<sv-date-picker [(ngModel)]="start" />

The week starts where the reader expects

locale sets the first column and the headings together, from CLDR — en-US Sunday, en-GB Monday, ar-EG Saturday. Omitting it keeps Monday and the English headings, so an upgrade does not move an existing caller's columns. weekStart overrides the tag when the host knows better.

example.htmlAngular
<sv-date-picker locale="en-US" [(value)]="start" />

<!-- pinned to a business week regardless of the tag -->
<sv-date-picker locale="ar-EG" [weekStart]="0" [(value)]="start" />

A rule for unavailable dates, not a list

dateDisabled takes a predicate, so an infinite set (no weekends, nothing inside 48 hours) does not have to be enumerated. It is OR-ed with minDate/maxDate, and it is called once per rendered cell — 42 per month view — so keep it pure and cheap.

example.htmlAngular
noWeekends = (d: Date) => d.getDay() === 0 || d.getDay() === 6;

<sv-date-picker [dateDisabled]="noWeekends" minDate="2026-01-01" [(value)]="start" />

When to use

  • Selecting a single calendar date with month/year navigation.
  • You need min/max constraints and disabled-date handling.
  • For time-of-day use the Time Picker; for ranges, compose two pickers.

Async states

None — this component has nothing to load, so it has no loading, empty or error state to drive.

Inputs

NameTypeDefaultDescription
valuestring (two-way)''Two-way bindable `YYYY-MM-DD` date string. A CALENDAR DATE, NOT AN INSTANT: A birthday is not a moment; it is the same date everywhere on Earth. That is why this is a string and not a `Date` — a `Date` is an instant, and storing one for "15 June" means the value shifts across midnight for half the world. ⚠️ COMPOSING THIS WITH `sv-time-picker` IS NOT `new Date(d + 'T' + t)`. That composes in the browser's zone, silently, and twice a year it composes something that does not exist: London, 2026-03-29, the clocks go 01:00 → 02:00 and `new Date('2026-03-29T01:30')` returns 02:30 without a word. Use `svComposeDateTime` from `@styloviz/core`, which reports `'gap'` and `'ambiguous'` instead of guessing.
labelstring''Label rendered above the trigger.
placeholderstring'Select date'Placeholder shown when no date is selected.
minDatestring''Minimum selectable date (YYYY-MM-DD). Cells before it are disabled.
maxDatestring''Maximum selectable date (YYYY-MM-DD). Cells after it are disabled.
localestring''BCP-47 tag for the week's first day and the column headings.
weekStartnumber | nullnullOverride the first day of the week. `0` is Sunday.
timeZonestring''Which zone's **today** this calendar means. THE ONE PLACE A CALENDAR DATE CANNOT AVOID A TIME ZONE. The value here is `YYYY-MM-DD` — no time, no zone, the same date everywhere. But "which date is today" has no zone-free answer: for several hours of every day, two places on Earth are on different dates. Every "today" in this component — the highlighted cell, the Today button, the month the panel opens on, the keyboard's starting point — was computed with `new Date()` in the BROWSER'S zone. That is right for a personal calendar and wrong for an app pinned to a business zone: a Sydney user booking against a New York ledger sees tomorrow highlighted as today, and the Today button writes a date the server rejects. An IANA zone (`'America/New_York'`). Empty means the browser's own, which is what this component was silently doing before the input existed — so leaving it unset changes nothing. ⚠️ It does NOT convert the value. A calendar date has no zone to convert between; use `svComposeDateTime` when you need an instant.
dateDisabled((date: Date) => boolean) | nullnullA rule for which dates cannot be chosen — weekends, blackout ranges, lead times.
disabledbooleanfalseDisables the trigger and prevents the popover from opening.
requiredbooleanfalseShows a red asterisk in the label.
ariaLabelstring''Explicit accessible name for the trigger (falls back to `label`).
errorMessagestring''Client-side validation message.
serverErrorSvServerErrornullA rejection reported by the server about this value. Self-expiring.

Outputs

EventPayload typeDescription
openedvoidEmitted when the calendar popover opens.
closedvoidEmitted when the calendar popover closes.
valueChangestringEmits the new `value` whenever it changes — the output half of `[(value)]`.

<sv-date-picker-pro>

Also in this family — SvDatePickerProComponent from @styloviz-pro/input-field.

Async states

None — this component has nothing to load, so it has no loading, empty or error state to drive.

Inputs

NameTypeDefaultDescription
valuestring (two-way)''Two-way bindable `YYYY-MM-DD` date string. A CALENDAR DATE, NOT AN INSTANT: A birthday is not a moment; it is the same date everywhere on Earth. That is why this is a string and not a `Date` — a `Date` is an instant, and storing one for "15 June" means the value shifts across midnight for half the world. ⚠️ COMPOSING THIS WITH `sv-time-picker` IS NOT `new Date(d + 'T' + t)`. That composes in the browser's zone, silently, and twice a year it composes something that does not exist: London, 2026-03-29, the clocks go 01:00 → 02:00 and `new Date('2026-03-29T01:30')` returns 02:30 without a word. Use `svComposeDateTime` from `@styloviz/core`, which reports `'gap'` and `'ambiguous'` instead of guessing.
labelstring''Label rendered above the trigger.
placeholderstring'Select date'Placeholder shown when no date is selected.
minDatestring''Minimum selectable date (YYYY-MM-DD). Cells before it are disabled.
maxDatestring''Maximum selectable date (YYYY-MM-DD). Cells after it are disabled.
localestring''BCP-47 tag for the week's first day and the column headings.
weekStartnumber | nullnullOverride the first day of the week. `0` is Sunday.
timeZonestring''Which zone's **today** this calendar means. THE ONE PLACE A CALENDAR DATE CANNOT AVOID A TIME ZONE. The value here is `YYYY-MM-DD` — no time, no zone, the same date everywhere. But "which date is today" has no zone-free answer: for several hours of every day, two places on Earth are on different dates. Every "today" in this component — the highlighted cell, the Today button, the month the panel opens on, the keyboard's starting point — was computed with `new Date()` in the BROWSER'S zone. That is right for a personal calendar and wrong for an app pinned to a business zone: a Sydney user booking against a New York ledger sees tomorrow highlighted as today, and the Today button writes a date the server rejects. An IANA zone (`'America/New_York'`). Empty means the browser's own, which is what this component was silently doing before the input existed — so leaving it unset changes nothing. ⚠️ It does NOT convert the value. A calendar date has no zone to convert between; use `svComposeDateTime` when you need an instant.
dateDisabled((date: Date) => boolean) | nullnullA rule for which dates cannot be chosen — weekends, blackout ranges, lead times.
disabledbooleanfalseDisables the trigger and prevents the popover from opening.
requiredbooleanfalseShows a red asterisk in the label.
ariaLabelstring''Explicit accessible name for the trigger (falls back to `label`).
errorMessagestring''Client-side validation message.
serverErrorSvServerErrornullA rejection reported by the server about this value. Self-expiring.

Outputs

EventPayload typeDescription
openedvoidEmitted when the calendar popover opens.
closedvoidEmitted when the calendar popover closes.
valueChangestringEmits the new `value` whenever it changes — the output half of `[(value)]`.

<sv-date-range-picker>

Also in this family — SvDateRangePickerComponent from @styloviz-pro/input-field.

Async states

None — this component has nothing to load, so it has no loading, empty or error state to drive.

Inputs

NameTypeDefaultDescription
startDatestring (two-way)''Start date YYYY-MM-DD. Supports two-way binding.
endDatestring (two-way)''End date YYYY-MM-DD. Supports two-way binding.
variantDateRangeVariant'default'Visual variant.
labelstring''Label above the trigger.
placeholderstring'Select date range'Trigger placeholder text.
minDatestring''Minimum selectable date (YYYY-MM-DD).
maxDatestring''Maximum selectable date (YYYY-MM-DD).
disabledbooleanfalseDisables the picker.
requiredbooleanfalseShows a required asterisk.
presetsDateRangePreset[]this._builtInPresetsCustom preset list. Defaults to built-in 8 presets. Only used by `with-presets` variant.
localestring''BCP 47 locale for the weekday headings and the first day of the week — `'en-US'` starts on Sunday, `'en-GB'` on Monday, `'ar-EG'` on Saturday. The same input, with the same default, as `sv-date-picker-pro`. A range picker and a date picker on one screen that disagree about which column is Sunday is the worst version of this bug: the user learns one grid and misreads the other. ⚠️ `''` MEANS "KEEP MONDAY", NOT "ASK THE BROWSER" — so an upgrade moves nobody's columns until the host asks it to.
weekStartnumber | nullnullOverride the first day of the week. `0` is Sunday.
timeZonestring''Which zone's **today** this calendar means — the ringed cell, the month the panel opens on, and the built-in presets ("Today", "Last 7 days", …). An IANA zone such as `'America/New_York'`. A preset is the place this matters most. "Last 7 days" computed in the browser's zone for a report pinned to a business zone is off by a day for several hours of every day, and nothing on screen says so. ⚠️ It never converts `startDate` / `endDate` — a calendar date has no zone to convert between. A host-supplied `presets` list is used as given.
dateDisabled((date: Date) => boolean) | nullnullA rule for which dates cannot be chosen — weekends, blackout days, lead times.

Outputs

EventPayload typeDescription
rangeChangeDateRangeChangeEventEmitted when both start and end are selected.
startDateChangestringEmits the new `startDate` whenever it changes — the output half of `[(startDate)]`.
endDateChangestringEmits the new `endDate` whenever it changes — the output half of `[(endDate)]`.

Customization & theming

  • **The value is a calendar date, not an instant** — a `YYYY-MM-DD` string, never a `Date`. A birthday is not a moment; it is the same date everywhere on Earth, and a `Date` for "15 June" shifts across midnight for half the world. `@styloviz/core` names the policy: `SvCalendarDate`, `svIsCalendarDate` (which rejects `2026-02-30`, where `new Date()` quietly returns 2 March), `svAddCalendarDays` (UTC arithmetic, so a DST boundary cannot move the day) and `SvDateValueAdapter`, the seam a Temporal or Luxon migration swaps behind.
  • `timeZone` says whose **today** the calendar means — the one place a calendar date cannot avoid a zone, because for several hours of every day two places on Earth are on different dates. It drives the ringed cell, the Today button, the month the panel opens on and the keyboard’s starting point. Unset means the browser’s zone, which is what the component always did, so leaving it alone changes nothing. ⚠️ It never converts the value: a calendar date has no zone to convert between. To build an instant, compose with `svComposeDateTime`, which reports the DST gaps and ambiguities instead of guessing.
  • Three views: day grid, month grid and year grid, with a Today / Clear footer.
  • Two-way bind `[(value)]` (YYYY-MM-DD) or use a reactive control (CVA); constrain with `minDate` / `maxDate`; set `label`, `ariaLabel`, `placeholder`, `required`, `disabled`.
  • React to `opened` / `closed`; today and disabled dates are highlighted with full dark-mode support.
  • `locale` drives the first day of the week and the column headings together, from CLDR (`Intl.Locale#getWeekInfo`) rather than a table; `weekStart` (0 = Sunday) overrides it. Both default to the kit's original Monday-first English headings so an upgrade changes nothing until you ask it to.
  • `dateDisabled` is a `(date: Date) => boolean` rule, OR-ed with the `minDate`/`maxDate` window. It gates day cells only — a month is not unavailable because some of its days are, and the month/year views are navigation rather than selection.

Best practices

  • Do: Constrain selectable dates with `minDate` / `maxDate` (YYYY-MM-DD) to prevent invalid input.
  • Do: Set `locale` when your users are not all on a Monday week. A calendar is read by column position before it is read by number, so a Sunday-first reader on a Monday-first grid picks the wrong date confidently — and the component reports exactly what they clicked.
  • Do: Express blackout rules as a `dateDisabled` predicate rather than enumerating dates. A date merely absent from a list looks exactly like a date that is allowed.
  • Don't: Don't fetch inside `dateDisabled`. It runs once per rendered cell, 42 per month view, on every navigation.
  • Do: Provide a `label` or `ariaLabel` so the trigger has a clear accessible name.
  • Do: Bind with `formControlName` / `[(ngModel)]` for validation and dirty/touched tracking; `[(value)]` remains available for lightweight use.
  • Don't: Don't force calendar-only entry for power users who prefer typing the date.

Accessibility

  • The calendar is a true `role="grid"`: weekday column headers, `role="row"` weeks and `role="gridcell"` day cells that carry `aria-selected`, so screen readers announce row/column position and the current selection.
  • Keyboard navigable: Arrow keys by day, Home/End to the start and end of the week AS DISPLAYED (both were hardcoded to a Monday week, which put Home and End on the same cell for a Sunday-first grid), PageUp/PageDown by month (Shift for year), Enter/Space to select. Opening moves focus into the grid (roving tabindex — a single tab stop); Escape and selection close the popover and return focus to the trigger.
  • Each day exposes a full-date `aria-label` and `aria-current="date"` for today; the trigger reports `aria-expanded` and an `ariaLabel`-based name. The Clear control is a real button (no nested interactive elements).
  • Implements ControlValueAccessor — works with `[(ngModel)]`, `[formControl]` and `formControlName`; `formControl.disable()` disables the trigger.

Feedback on Date Picker

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.