Datepicker
Calendar and date picker popover with input or button toggles, single, multiple, and range date selection, and multi-month layouts.
Introduction
The Datepicker wraps Vanilla Calendar Pro to render a calendar as an input-attached popover, a button-triggered popover, or an always-visible inline calendar. It supports single, multiple, and ranged date selection, adapts to Chassis's color modes, and exposes any Vanilla Calendar Pro setting through a vcpOptions pass-through.
Basic structure
Add data-cx-toggle="datepicker" to a text input to initialize it as a datepicker trigger. Use type="text" rather than a native date input type so the browser's built-in date picker UI doesn't compete with the calendar popover.
<label for="datepicker1" class="form-label">Datepicker</label>
<input type="text" class="form-input" id="datepicker1" data-cx-toggle="datepicker" placeholder="Choose date…">By default the calendar opens below the input on focus. Selecting a date updates the input's value — single-selection and completed range pickers close automatically, while multiple-date pickers stay open for further selection.
Layout options
Compose the datepicker input with other form markup for labels, help text, and icon adornments.
With field
Use the Field component to lay out and wrap inputs with a label, description, and validation feedback messages in a vertical stack.
<div class="form-field">
<label for="datepicker2" class="form-label">Datepicker field</label>
<input type="text" class="form-input" id="datepicker2" data-cx-toggle="datepicker" placeholder="Choose date…">
<div class="form-text">
We’ll never share your email with anyone else.
</div>
</div>With icon
Use the Input Adorn component to add a calendar icon alongside the datepicker input. Use a <button> for the adorn element and focus the input on click so the calendar opens.
<label for="datepickerIconStart" class="form-label">Select date</label>
<div class="form-input">
<input type="text" class="ghost-input" id="datepickerIconStart" data-cx-toggle="datepicker" placeholder="Choose date…">
<button type="button" class="input-adorn" onclick="this.parentElement.querySelector('.ghost-input').focus()" aria-label="Open calendar">
<svg class="icon" aria-hidden="true"><use href="#calendar-solid"></use></svg>
</button>
</div>The calendar positions itself relative to the input element by default. Pass a positionElement selector or element to anchor the popover to a different ancestor instead (see Options).
Date constraints
Restrict the selectable date range using data-cx-date-min and data-cx-date-max.
<label for="datepickerMinMax" class="form-label">Event date (2026 only)</label>
<input type="text" class="form-input" id="datepickerMinMax" data-cx-toggle="datepicker" data-cx-date-min="2026-01-01" data-cx-date-max="2026-12-31" placeholder="Select a date in 2026">Selection modes
Set data-cx-selection-mode to control how many dates a picker accepts: single (default), multiple, or multiple-ranged.
Multiple dates
Enable multiple date selection with data-cx-selection-mode="multiple".
<label for="datepicker3" class="form-label">Select multiple dates</label>
<input type="text" class="form-input" id="datepicker3" data-cx-toggle="datepicker" data-cx-selection-mode="multiple" placeholder="Select date range…">Date range
Select a range of dates with data-cx-selection-mode="multiple-ranged". Use data-cx-selected-dates to preselect a date range.
<label for="datepicker4" class="form-label">Select date range</label>
<input type="text" class="form-input" id="datepicker4" data-cx-toggle="datepicker" data-cx-selection-mode="multiple-ranged" data-cx-selected-dates='["2026-06-18", "2026-07-07"]' placeholder="Select start and end dates…">Multi-month layout
Display multiple months side-by-side with data-cx-display-months-count. This gives date-range pickers enough width to show both ends of the range without navigating between months.
Multiple selection
Combine data-cx-selection-mode="multiple" with data-cx-display-months-count="2" to let a picker select individual dates that fall in different months.
<label for="datepickerMultiMonth" class="form-label">Select dates</label>
<input type="text" class="form-input" id="datepickerMultiMonth" data-cx-toggle="datepicker" data-cx-selection-mode="multiple" data-cx-display-months-count="2" placeholder="Select dates">Range selection
Combine data-cx-selection-mode="multiple-ranged" with data-cx-display-months-count="2" to preselect a range that spans a month boundary.
<label for="datepickerRangeTwoMonths" class="form-label">Select date range</label>
<input type="text" class="form-input" id="datepickerRangeTwoMonths" data-cx-toggle="datepicker" data-cx-selection-mode="multiple-ranged" data-cx-display-months-count="2" data-cx-selected-dates='["2026-06-16", "2026-07-07"]' placeholder="Select start and end dates…">Advanced features
Secondary options that fine-tune calendar behavior — which day starts the week and where the popover opens relative to the input.
First day of week
Set the first day of the week (0 = Sunday, 1 = Monday, etc.) with data-cx-first-weekday.
<label for="datepicker6" class="form-label">Week starts on Sunday</label>
<input type="text" class="form-input" id="datepicker6" data-cx-toggle="datepicker" data-cx-first-weekday="0" placeholder="Select a date">Placement
Control where the calendar appears relative to the input with data-cx-placement. Options are left (default), center, right, and auto.
<div class="d-flex gap-md">
<div>
<label for="datepickerLeft" class="form-label">Left aligned</label>
<input type="text" class="form-input" id="datepickerLeft" data-cx-toggle="datepicker" data-cx-placement="left" placeholder="Left">
</div>
<div>
<label for="datepickerCenter" class="form-label">Center aligned</label>
<input type="text" class="form-input" id="datepickerCenter" data-cx-toggle="datepicker" data-cx-placement="center" placeholder="Center">
</div>
<div>
<label for="datepickerRight" class="form-label">Right aligned</label>
<input type="text" class="form-input" id="datepickerRight" data-cx-toggle="datepicker" data-cx-placement="right" placeholder="Right">
</div>
</div>Button trigger
Use a button instead of an input for use cases like dashboard date filters. Add data-cx-datepicker-display to the text element to preserve icons when the date updates.
<button type="button" class="button outline secondary" data-cx-toggle="datepicker">
<svg class="icon" aria-hidden="true"><use href="#calendar-solid"></use></svg>
<span data-cx-datepicker-display>Select date</span>
</button>For date range selection (e.g., dashboard time filters), use data-cx-selection-mode="multiple-ranged". The picker closes once both the start and end dates are selected.
<button type="button" class="button outline secondary" data-cx-toggle="datepicker" data-cx-selection-mode="multiple-ranged">
<svg class="icon" aria-hidden="true"><use href="#calendar-solid"></use></svg>
<span data-cx-datepicker-display>Select dates</span>
</button>Display the selected date in a separate element with the displayElement option:
import { Datepicker } from '@chassis-ui/css'
const datepicker = new Datepicker(buttonElement, {
selectionMode: 'multiple-ranged',
displayElement: '#date-display' // Selector or element
})Inline calendar
Render the calendar inline (always visible, no popup) with data-cx-inline="true". Use this to embed a calendar directly in the page instead of behind a trigger.
<div data-cx-toggle="datepicker" data-cx-inline="true"></div>Inline datepickers support the same selection modes as popover datepickers:
<div data-cx-toggle="datepicker" data-cx-inline="true" data-cx-selection-mode="multiple-ranged"></div>Multiple months inline:
<div data-cx-toggle="datepicker" data-cx-inline="true" data-cx-display-months-count="2"></div>Bind to form
Include a hidden input inside the container to bind the selection to a form field. The plugin updates its value with the selected date(s) in YYYY-MM-DD format (comma-separated for multiple dates):
<form>
<div data-cx-toggle="datepicker" data-cx-inline="true">
<input type="hidden" name="selected_date">
</div>
<button type="submit" class="button solid primary mt-md">Submit</button>
</form>Custom date formatting
Control how dates are displayed using the dateFormat option. Pass an Intl.DateTimeFormat options object or a function(date, locale).
import { Datepicker } from '@chassis-ui/css'
// Using Intl.DateTimeFormat options
const datepicker = new Datepicker(element, {
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' }
// Output: "Dec 23, 2026 – Dec 28, 2026"
})
// Using a custom function
const datepickerCustom = new Datepicker(element, {
dateFormat: (date, locale) => date.toLocaleDateString(locale, { month: 'short', day: 'numeric' })
// Output: "Dec 23 – Dec 28"
})Theming
The datepicker adapts to Chassis's color modes. When data-cx-theme="dark" is set on a parent element or the <html> element, the calendar popup inherits that theme.
Inherited from parent
When a parent element carries a theme, both the input and calendar popup inherit it:
<div data-cx-theme="dark" class="p-md bg-main p-md rounded">
<label for="datepickerDark" class="form-label">Dark mode datepicker</label>
<input type="text" class="form-input" id="datepickerDark" data-cx-toggle="datepicker" placeholder="Select a date">
</div>Datepicker-only theme
Use data-cx-datepicker-theme to set the datepicker popup’s theme independently of the input — for example, a light input paired with a dark datepicker, or the reverse:
<label for="datepickerTheme" class="form-label">Light input, dark datepicker</label>
<input data-cx-toggle="datepicker" data-cx-datepicker-theme="dark" type="text" class="form-input" id="datepickerTheme" placeholder="Select a date">Accessibility
Vanilla Calendar Pro marks calendar day state with ARIA attributes: the selected day carries aria-selected="true", disabled days carry aria-disabled="true", and today's date carries aria-current="date". Chassis styles each of these states directly from that generated markup — see scss/_datepicker.scss.
Chassis doesn't add aria-expanded or aria-haspopup to the trigger element. Add them manually, and keep aria-expanded in sync with the show.cx.datepicker / hide.cx.datepicker events, if the trigger needs to announce popover state to assistive technology.
JavaScript API
The Datepicker plugin wraps Vanilla Calendar Pro and exposes its configuration through Chassis-managed options, data attributes, and events. Chassis JS ships as an ES module — import the Datepicker class:
import { Datepicker } from '@chassis-ui/css'Triggers
Add data-cx-toggle="datepicker" to any input or button element to initialize it as a datepicker without writing JavaScript.
| Attribute | Description |
|---|---|
data-cx-toggle="datepicker" | Initializes the datepicker on the input (or button) element. |
data-cx-inline | When true, renders the calendar inline instead of a popup. |
<input type="text" class="form-input" data-cx-toggle="datepicker">Initialization
For programmatic access — calling methods or listening to events — instantiate each element with the Datepicker class:
const datepickerEl = document.getElementById('myDatepicker')
const datepicker = new Datepicker(datepickerEl, {
selectionMode: 'single',
firstWeekday: 1
})Methods
The plugin exposes instance methods for programmatic control, plus the static lookup methods shared by every Chassis component:
| Method | Description |
|---|---|
show() | Shows the datepicker calendar |
hide() | Hides the datepicker calendar |
toggle() | Toggles the datepicker visibility |
getSelectedDates() | Returns an array of selected dates in YYYY-MM-DD format |
setSelectedDates(dates) | Sets the selected dates. Expects an array of YYYY-MM-DD strings |
dispose() | Destroys the datepicker instance |
getInstance(element) | Static method to get the datepicker instance from a DOM element |
getOrCreateInstance(element) | Static method to get or create a datepicker instance |
Options
Options are set via data-cx-* attributes or passed to the constructor as an object:
Options are set via data-cx-* attributes or passed to the constructor as an object. Attribute names use the kebab-case form of the option name — data-cx-custom-class, not data-cx-customClass. Attribute values are parsed to their native types:"true" → true, "0" → 0, and valid JSON strings to objects.
Use data-cx-config to pass multiple options as a JSON string:data-cx-config='{"delay":200}'. Individual data-cx-* attributes take precedence over data-cx-config. You can also use JSON values in individual attributes, such as data-cx-delay='{"show":100,"hide":200}'.
When initializing components, Chassis merges configurations from multiple sources in this priority order: default settings, data-cx-config values, individual data-cx-*attributes, and finally any JavaScript object options. Values defined later in this sequence override earlier ones.
| Name | Type | Default | Description |
|---|---|---|---|
dateMin | string, number, Date | null | Minimum selectable date. Format: YYYY-MM-DD |
dateMax | string, number, Date | null | Maximum selectable date. Format: YYYY-MM-DD |
dateFormat | object, function | null | Date formatting. Pass Intl.DateTimeFormat options or a function(date, locale). |
displayElement | string, element, boolean | null | Element to show formatted date. For buttons, defaults to the button itself. Set to false to disable. |
displayMonthsCount | number | 1 | Number of months to display side-by-side in the calendar. |
firstWeekday | number | 1 | First day of week (0 = Sunday, 1 = Monday, etc.) |
inline | boolean | false | Render calendar inline (always visible, no popup). |
locale | string | browser language (2-letter) | Locale for date formatting (e.g., 'en-US', 'de-DE'). Pass 'default' to use the runtime's default locale formatting. |
positionElement | string, element | null | Element to position the calendar relative to. Checks for a .form-adorn ancestor first, then falls back to the input (or button) itself. |
selectedDates | array | [] | Pre-selected dates in YYYY-MM-DD format |
selectionMode | string | 'single' | Selection mode: 'single', 'multiple', or 'multiple-ranged' |
placement | string | 'left' | Calendar position relative to input: 'left', 'center', 'right', 'auto' |
datepickerTheme | string | null | Force datepicker popup theme: 'light', 'dark', 'auto', or null to inherit from ancestor [data-cx-theme] |
vcpOptions | object | {} | Pass-through object for any Vanilla Calendar Pro option |
Advanced configuration
For settings not directly exposed by Chassis's options, pass any Vanilla Calendar Pro setting through vcpOptions. Chassis-managed options — like firstWeekday, selectionMode, and locale — take precedence over the same key inside vcpOptions, so reserve it for settings without a dedicated Chassis option:
const datepicker = new Datepicker(element, {
vcpOptions: {
disableDatesPast: true, // Disable past dates
disableWeekdays: [0, 6], // Disable weekends
disableDates: ['2026-12-25', '2026-12-26'], // Disable specific dates
selectedHolidays: ['2026-01-01'], // Highlight holidays
selectionTimeMode: 24 // Enable 24-hour time selection
}
})See the Vanilla Calendar Pro documentation for all available options.
Events
The plugin fires events around the show/hide lifecycle and date selection:
| Event | Description |
|---|---|
show.cx.datepicker | Fires immediately when the show method is called |
shown.cx.datepicker | Fires when the datepicker has been made visible |
hide.cx.datepicker | Fires immediately when the hide method is called |
hidden.cx.datepicker | Fires when the datepicker has been hidden |
change.cx.datepicker | Fires when a date is selected. Event includes dates (array) and event properties |
const datepickerEl = document.getElementById('myDatepicker')
datepickerEl.addEventListener('change.cx.datepicker', event => {
console.log('Selected dates:', event.dates)
})CSS
The Datepicker component can be customized at both runtime (via custom properties) and compile time (via Sass variables).
Custom properties
The Datepicker component exposes CSS custom properties to control its appearance at runtime.
--cx-padding-x: var(--cx-datepicker-padding-x, 0.5rem);
--cx-padding-y: var(--cx-datepicker-padding-y, 0.5rem);
--cx-gap: var(--cx-datepicker-gap, 0.25rem);
--cx-bg-color: var(--cx-datepicker-bg-color, var(--cx-default-bg-main));
--cx-fg-color: var(--cx-datepicker-fg-color, var(--cx-default-fg-main));
--cx-border-color: var(--cx-datepicker-border-color, var(--cx-default-transparent-color));
--cx-border-width: var(--cx-datepicker-border-width, var(--cx-border-width-md));
--cx-border-radius: var(--cx-datepicker-border-radius, var(--cx-border-radius-lg));
--cx-day-border-radius: var(--cx-datepicker-day-border-radius, var(--cx-border-radius-md));
--cx-box-shadow: var(--cx-datepicker-box-shadow, var(--cx-box-shadow-md));
--cx-font-size: var(--cx-datepicker-font-size, var(--cx-font-size-sm));
--cx-today-font-weight: var(--cx-datepicker-today-font-weight, var(--cx-font-weight-strong));
--cx-day-width: var(--cx-datepicker-day-width, 2rem);
--cx-week-width: var(--cx-datepicker-week-width, 14rem);
--cx-zindex: var(--cx-datepicker-zindex, 1000);
--cx-header-font-size: var(--cx-datepicker-header-header-font-size, var(--cx-font-size-sm));
--cx-header-font-weight: var(--cx-datepicker-header-header-font-weight, var(--cx-font-weight-strong));
--cx-arrow-icon: var(--cx-datepicker-arrow-icon, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24'%3e%3cpath d='M12 16c-.3 0-.5-.1-.7-.3l-6-6c-.4-.4-.4-1 0-1.4s1-.4 1.4 0l5.3 5.3 5.3-5.3c.4-.4 1-.4 1.4 0s.4 1 0 1.4l-6 6c-.2.2-.4.3-.7.3'/%3e%3c/svg%3e"));
--cx-weekday-fg-color: var(--cx-datepicker-weekday-fg-color, var(--cx-default-fg-main));
--cx-weekday-font-size: var(--cx-datepicker-weekday-font-size, var(--cx-font-size-text-xs));
--cx-day-hover-bg-color: var(--cx-datepicker-day-hover-bg-color, var(--cx-default-bg-even));
--cx-day-selected-bg-color: var(--cx-datepicker-day-selected-bg-color, var(--cx-primary-base-color));
--cx-day-selected-fg-color: var(--cx-datepicker-day-selected-fg-color, var(--cx-primary-contrast-color));
--cx-day-range-bg-color: var(--cx-datepicker-day-range-bg-color, var(--cx-default-bg-highlight));
--cx-day-range-fg-color: var(--cx-datepicker-day-range-fg-color, var(--cx-default-fg-idle));
--cx-day-today-bg-color: var(--cx-datepicker-day-today-bg-color, var(--cx-default-transparent-color));
--cx-day-today-fg-color: var(--cx-datepicker-day-today-fg-color, var(--cx-default-cue-main));
--cx-day-weekend-bg-color: var(--cx-datepicker-day-weekend-bg-color, var(--cx-default-transparent-color));
--cx-day-weekend-fg-color: var(--cx-datepicker-day-weekend-fg-color, var(--cx-default-fg-subtle));
--cx-day-disabled-fg-color: var(--cx-datepicker-day-disabled-fg-color, var(--cx-default-fg-slight));Sass variables
The Datepicker component uses Sass variables in scss/config/_defaults.scss to define its defaults; they are also exposed as CSS custom properties using the --cx- prefix for runtime override — $variable-name becomes --cx-variable-name. See the component anatomy page.
$datepicker-arrow-icon: "<svg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24'><path d='M12 16c-.3 0-.5-.1-.7-.3l-6-6c-.4-.4-.4-1 0-1.4s1-.4 1.4 0l5.3 5.3 5.3-5.3c.4-.4 1-.4 1.4 0s.4 1 0 1.4l-6 6c-.2.2-.4.3-.7.3'/></svg>";
$datepicker-weekday-font-size: $font-size-xs;Design tokens
The Datepicker component consumes design tokens from Chassis Tokens with the $cx- prefix. — See thedesign tokens page.
$datepicker-fg-idle: $cx-color-datepicker-fg-idle;
// $datepicker-bg-idle: $cx-color-datepicker-bg-idle;
// $datepicker-fg-hover: $cx-color-datepicker-fg-hover;
$datepicker-bg-hover: $cx-color-datepicker-bg-hover;
// $datepicker-fg-press: $cx-color-datepicker-fg-press;
// $datepicker-bg-press: $cx-color-datepicker-bg-press;
$datepicker-fg-active: $cx-color-datepicker-fg-active;
$datepicker-bg-active: $cx-color-datepicker-bg-active;
$datepicker-fg-range: $cx-color-datepicker-fg-range;
$datepicker-bg-range: $cx-color-datepicker-bg-range;
$datepicker-fg-today: $cx-color-datepicker-fg-today;
$datepicker-bg-today: $cx-color-datepicker-bg-today;
$datepicker-fg-weekend: $cx-color-datepicker-fg-weekend;
$datepicker-bg-weekend: $cx-color-datepicker-bg-weekend;
$datepicker-fg-outside: $cx-color-datepicker-fg-outside;
// $datepicker-bg-outside: $cx-color-datepicker-bg-outside;
$datepicker-fg-main: $cx-color-datepicker-fg-main;
$datepicker-bg-main: $cx-color-datepicker-bg-main;
$datepicker-border-main: $cx-color-datepicker-border-main;
// $datepicker-border-subtle: $cx-color-datepicker-border-subtle;
$datepicker-border-radius-main: $cx-border-radius-datepicker-main;
$datepicker-border-radius-day: $cx-border-radius-datepicker-day;
// $datepicker-border-radius-menu: $cx-border-radius-datepicker-menu;
$datepicker-font-day: $cx-font-datepicker-day;
$datepicker-font-today: $cx-font-datepicker-today;
$datepicker-font-label: $cx-font-datepicker-label;
$datepicker-border-width-main: $cx-border-width-datepicker-main;
$datepicker-shadow-main: $cx-shadow-datepicker-main;
$datepicker-day-width: $cx-size-datepicker-day-width;
// $datepicker-day-height: $cx-size-datepicker-day-height;
$datepicker-week-width: $cx-size-datepicker-week-width;
// $datepicker-menu-width: $cx-size-datepicker-menu-width;
$datepicker-padding-y: $cx-space-datepicker-padding-y;
$datepicker-padding-x: $cx-space-datepicker-padding-x;
$datepicker-gap: $cx-space-datepicker-gap;