A zero-dependency date picker for vanilla JavaScript. Drop in one script tag, point it at an input, done. It follows your page's light or dark theme on its own — flip the switch above and watch.
<script src="https://cdn.jsdelivr.net/gh/vanilla-ui-kit/components/datepicker/datepicker.js"></script>
<input id="date">
<script>new DatePicker('#date')</script>
Every card is live — click into the inputs.
Defaults: ISO format, browser locale, automatic theme.
new DatePicker('#ex-basic')
Token formats, month names from Intl — no locale bundles.
new DatePicker('#ex-format', { format: 'MMMM D, YYYY' })
new DatePicker('#ex-locale', { format: 'DD/MM/YYYY', locale: 'fr' })
Two clicks, hover preview, one input.
new DatePicker('#ex-range', { range: true })
Side-by-side months with a quick-select rail — the booking-site classic.
new DatePicker('#ex-multipane', {
range: true,
panes: 2,
presets: true // or your own [{label, range}]
})
The whole thing — range, two panes, presets — from attributes alone.
<input data-datepicker data-range
data-panes="2"
data-presets="last7,last30,thisMonth,lastMonth">
Bookable window: today through +90 days, weekends off.
new DatePicker('#ex-min', {
min: 'today',
max: new Date(Date.now() + 90 * 864e5),
disabledDays: [0, 6]
})
ISO-8601 week numbers in the left rail.
new DatePicker('#ex-weeks', { weekNumbers: true, firstDay: 1 })
Pin a theme regardless of the page, or restyle the accent.
new DatePicker('#ex-dark', { theme: 'dark' })
new DatePicker('#ex-accent', { accent: '#0f766e' })
Data attributes only — no script written for this input.
<input data-datepicker
data-format="DD MMM YYYY"
data-min="today">
Callbacks, custom events, and instance methods.
const dp = new DatePicker('#ex-events', {
onSelect: (date, text) => show(text)
})
dp.open()
dp.setDate('today')
dp.getDate() // Date object
Point it at a container instead of an input.
<div id="ex-inline"></div>
new DatePicker('#ex-inline', {
onSelect: (d, text) => console.log(text)
})
new DatePicker(target, options?) // target: element or CSS selector
<input>: the calendar opens on focus/click and writes the formatted value back.inline: true): the calendar is always visible.range: true selects a start and end date in one input, joined by rangeSeparator.data-datepicker to an input; it is auto-initialized on load (see Declarative init).Constructing a second picker on the same element destroys the first. Also works with CommonJS/AMD (require('./datepicker.js')) and is SSR-safe (throws only when actually constructed without a DOM). Use type="text" inputs — on type="date" the browser enforces ISO values and may show its own picker.
Second constructor argument; all keys of DatePicker.defaults. Change a default for every picker with, e.g., DatePicker.defaults.format = 'DD/MM/YYYY', or per instance later via setOptions().
| Name | Type | Default | Description |
|---|---|---|---|
value | Date | 'today' | string | null | Initial value; strings are parsed in format or ISO. In range mode also [start, end] or {start, end}. |
format | string | 'YYYY-MM-DD' | Display/parse format. Tokens: YYYY YY MMMM MMM MM M DD D. |
min | Date | 'today' | string | null | Earliest selectable date. |
max | Date | 'today' | string | null | Latest selectable date. |
disabledDates | (Date|string)[] | fn | [] | Array of dates, or fn(date) => boolean returning true to disable. |
disabledDays | number[] | [] | Disabled weekdays, 0 = Sunday … 6 = Saturday. |
range | boolean | false | Range selection — “start – end” in one input, with hover preview. |
rangeSeparator | string | ' – ' | Text joining start and end in the input value. |
panes | number | 1 | Months shown side by side (1–3). With panes > 1 the masthead month/year quick views are disabled and the footer Today button is omitted (the presets rail owns quick jumps there). |
presets | boolean | string | array | null | Quick-select rail. true = built-ins; a comma string picks a subset by key ('last7,thisMonth'); or bring your own [{ label, range }] / [{ label, date }] (see Statics & helpers). |
inline | boolean | false | Always-visible calendar. Automatic when the target is not an input. |
locale | string | null | BCP-47 tag (e.g. 'fr'); defaults to the browser locale. All names come from Intl — no locale bundles. |
firstDay | number | null | 0–6; defaults to the locale’s first weekday. |
weekNumbers | boolean | false | ISO-8601 week number column. |
theme | string | 'auto' | 'auto' | 'light' | 'dark' (see Theming). |
styles | boolean | true | false = headless: no CSS is ever injected (see Theming). |
accent | string | null | Any CSS color for the accent (selection, today, buttons). |
todayButton | boolean | true | Show the footer Today button. |
clearButton | boolean | true | Show the footer Clear button. |
autoClose | boolean | true | Close the popup after picking a date (or a full range). |
position | string | 'auto' | 'auto' | 'below' | 'above' — popup placement relative to the input. |
labels | object | see description | UI strings for i18n. Defaults: { previous: 'Previous', next: 'Next', today: 'Today', clear: 'Clear', dialog: 'Choose date', switchView: 'Switch calendar view', presets: 'Quick select', weekAbbr: 'Wk' }. Partial overrides merge. |
onSelect | function | null | fn(value, formatted, picker) — see Events & callbacks. |
onOpen | function | null | fn(picker). |
onClose | function | null | fn(picker). |
onClear | function | null | fn(picker). |
onMonthChange | function | null | fn(viewDate, picker). |
Built-in preset keys: today, yesterday, tomorrow, last7, last30, thisMonth, lastMonth, thisYear. presets: true shows Today, Yesterday, Last 7 days, Last 30 days, This month, Last month in range mode — or Today/Yesterday/Tomorrow in single-date mode. Function-valued presets (range: () => [a, b]) are re-evaluated at click time, and the active preset is highlighted when the value matches it.
All methods return the instance (chainable) unless noted.
| Method | Returns | Description |
|---|---|---|
open() | this | Show the popup (no-op for inline calendars). |
close() | this | Hide the popup; returns focus to the input when focus was inside the panel. |
toggle() | this | Open if closed, close if open. |
getDate() | Date | {start, end} | null | Current value — a Date, or {start, end} in range mode. |
setDate(value, config?) | this | Set the value: Date | string | 'today' | null; in range mode [start, end] or {start, end}. Pass { silent: true } to skip onSelect, the datepicker:select event, and the input’s change event. |
clear() | this | Clear the value (fires onClear / datepicker:clear). |
setOptions(partial) | this | Change any option on the fly, e.g. dp.setOptions({ min: 'today' }); re-renders (and rebuilds the panel when chrome options like panes, presets, inline change). |
destroy() | this | Remove the panel and all listeners, and unregister the instance. |
| Member | Returns | Description |
|---|---|---|
DatePicker.create(target, options?) | DatePicker | Constructor alias — same as new DatePicker(…). |
DatePicker.get(target) | DatePicker | null | The instance bound to an element (or selector), or null. |
DatePicker.autoInit(root?) | DatePicker[] | Initialize every [data-datepicker] under root (default: document) that has no instance yet. Runs automatically on load; call it for content added later. |
DatePicker.formatDate(date, format?, locale?) | string | Pure helper — format a Date with the token format (default 'YYYY-MM-DD'), e.g. DatePicker.formatDate(new Date(), 'DD MMM YYYY', 'de'). |
DatePicker.parseDate(str, format?, locale?) | Date | null | Pure helper — parse a string in the token format; null for invalid or impossible dates (e.g. 2026-02-30). |
DatePicker.defaults | object | Global defaults — mutate once to affect every picker created afterwards. |
DatePicker.salt | string | false | CSS isolation namespace (default 'vc1'). Set your own token before the first picker, or false to disable salting. |
DatePicker.css | string | The full stylesheet, always rendered with the current salt — a starting point for headless styling. |
DatePicker.version | string | Library version. |
Option callbacks:
| Callback | Signature | Fires |
|---|---|---|
onSelect | fn(value, formatted, picker) | After a date (or complete range) is picked or set. value is a Date, or {start, end} in range mode; formatted is the input string. |
onOpen | fn(picker) | When the popup opens. |
onClose | fn(picker) | When the popup closes. |
onClear | fn(picker) | When the value is cleared. |
onMonthChange | fn(viewDate, picker) | When the visible month changes (arrows, paging, quick views). |
The bound input also fires native input/change events, plus bubbling CustomEvents (dispatched on the target element for inline calendars):
| Event | event.detail |
|---|---|
datepicker:select | { value, formatted, picker } |
datepicker:open | { picker } |
datepicker:close | { picker } |
datepicker:clear | { picker } |
input.addEventListener('datepicker:select', e => console.log(e.detail.value))
Mark an input with data-datepicker and it is initialized automatically on load (or later via DatePicker.autoInit(root)). Every option except the function-valued ones maps to a kebab-case attribute:
| Attribute | Option | Notes |
|---|---|---|
data-datepicker | — | Marks the element for auto-init. |
data-value | value | String in format, ISO, or today. |
data-format | format | e.g. DD/MM/YYYY. |
data-min / data-max | min / max | Date string or today. |
data-disabled-days | disabledDays | Comma list, e.g. "0,6". |
data-disabled-dates | disabledDates | Comma list, e.g. "2026-12-25,2026-12-26". |
data-range | range | Boolean flag; bare attribute = true, "false"/"0" = false. |
data-range-separator | rangeSeparator | |
data-panes | panes | Number 1–3. |
data-presets | presets | Bare attribute = built-ins; or a comma list of keys, e.g. "last7,thisMonth". |
data-inline | inline | Boolean flag. |
data-locale | locale | BCP-47 tag. |
data-first-day | firstDay | Number 0–6. |
data-week-numbers | weekNumbers | Boolean flag. |
data-theme | theme | auto / light / dark. |
data-styles | styles | data-styles="false" for headless. |
data-accent | accent | Any CSS color. |
data-today-button / data-clear-button | todayButton / clearButton | Boolean flags. |
data-auto-close | autoClose | Boolean flag. |
data-position | position | auto / below / above. |
<input data-datepicker data-range data-panes="2"
data-presets="last7,last30,thisMonth,lastMonth">
theme: 'auto' (the default) resolves from, in order: <html data-theme="dark"> or data-bs-theme (Bootstrap) → <html class="dark"> (Tailwind-style) → the OS prefers-color-scheme — and re-resolves live when any of those change. Set theme: 'light' | 'dark' to pin it. All colors are CSS custom properties on .vdp (scope per theme with .vdp[data-theme="dark"] { … }):
| Property | Role |
|---|---|
--vdp-accent | Primary color: selection, today, buttons. |
--vdp-on-accent | Text on the accent color. |
--vdp-accent-soft | Soft accent fill (in-range days, active preset). |
--vdp-accent-mist | Faintest accent wash (range hover preview). |
--vdp-bg | Panel background. |
--vdp-surface | Hover fill. |
--vdp-text | Text color. |
--vdp-muted | Secondary text (weekday header, week numbers). |
--vdp-faint | Borders. |
--vdp-shadow | Panel shadow. |
--vdp-radius | Panel corner radius. |
--vdp-cell | Day cell size. |
--vdp-font | UI font stack. |
--vdp-display-font | Masthead (month/year) font. |
CSS isolation — the panel renders as class="vdp vc1" and structural rules ship salted, so host-page resets and generic state classes can’t override the widget; variable definitions stay unsalted so the overrides above keep working. Set DatePicker.salt = 'acme' before the first picker, or DatePicker.salt = false to disable.
Headless — pass styles: false (or data-styles="false") and no CSS is ever injected: you keep the full markup, behavior, keyboard handling, and ARIA, with stable .vdp-* class hooks and state classes (is-selected, is-disabled, in-range, …). Start from DatePicker.css (string) or dist/datepicker.css. Injection is deduped page-wide, so keep styles: false on every instance when going headless.
| Key | Action |
|---|---|
↓ (on the input) | Open the calendar and move focus into the grid. |
← / → | Move focus by one day. |
↑ / ↓ | Move focus by one week. |
PageUp / PageDown | Previous / next month. |
Shift+PageUp / Shift+PageDown | Previous / next year. |
Home / End | First / last day of the week. |
Enter / Space | Select the focused day. |
Escape | Close and return focus to the input. |
Tab | Tabbing out of the widget closes it. |
ARIA dialog/grid roles, aria-selected, aria-current="date" on today; month changes are announced politely; prefers-reduced-motion is respected. Typing a date by hand always works — invalid text (including impossible dates like 2026-02-30) reverts on blur, and typed values respect min/max/disabled rules.
Pass styles: false and the picker never injects a byte of CSS —
you keep the full behavior, keyboard handling, and ARIA, plus the stable .vdp-*
class hooks and state classes (is-selected, is-disabled,
in-range, …) to style however you like. Both demos below run in an
<iframe>: the styled examples above have already injected the stylesheet
into this page, so a headless instance out here would still match those rules.
The raw browser rendering — semantic markup, native buttons, zero CSS. Keyboard and ARIA still work; try arrowing around the grid.
<script src="datepicker.js"></script>
<div id="cal"></div>
<script>
new DatePicker('#cal', { styles: false, value: 'today' }) // zero CSS injected
</script>
The same headless init with every structural .vdp-* hook mapped to Tailwind
utilities via @apply — a different product's design system, not a reskin of ours.
The button fetches cdn.tailwindcss.com, so nothing loads until you ask.