Vanilla UI Kit DatePicker v1.0.0

One file.
Every date.

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>

Examples

Every card is live — click into the inputs.

Basic

Defaults: ISO format, browser locale, automatic theme.

new DatePicker('#ex-basic')

Format & locale

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' })

Range selection

Two clicks, hover preview, one input.

new DatePicker('#ex-range', { range: true })

Two panes & presets

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}]
})

Zero-JS range picker

The whole thing — range, two panes, presets — from attributes alone.

<input data-datepicker data-range
       data-panes="2"
       data-presets="last7,last30,thisMonth,lastMonth">

Constraints

Bookable window: today through +90 days, weekends off.

new DatePicker('#ex-min', {
  min: 'today',
  max: new Date(Date.now() + 90 * 864e5),
  disabledDays: [0, 6]
})

Week numbers, Monday first

ISO-8601 week numbers in the left rail.

new DatePicker('#ex-weeks', { weekNumbers: true, firstDay: 1 })

Theme override & accent

Pin a theme regardless of the page, or restyle the accent.

new DatePicker('#ex-dark', { theme: 'dark' })
new DatePicker('#ex-accent', { accent: '#0f766e' })

Zero-JS auto-init

Data attributes only — no script written for this input.

<input data-datepicker
       data-format="DD MMM YYYY"
       data-min="today">

Events & API

Callbacks, custom events, and instance methods.

Nothing selected yet
const dp = new DatePicker('#ex-events', {
  onSelect: (date, text) => show(text)
})
dp.open()
dp.setDate('today')
dp.getDate() // Date object

Inline calendar

Point it at a container instead of an input.

<div id="ex-inline"></div>
new DatePicker('#ex-inline', {
  onSelect: (d, text) => console.log(text)
})

API reference

Constructor & usage

new DatePicker(target, options?)   // target: element or CSS selector

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.

Options

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().

NameTypeDefaultDescription
valueDate | 'today' | stringnullInitial value; strings are parsed in format or ISO. In range mode also [start, end] or {start, end}.
formatstring'YYYY-MM-DD'Display/parse format. Tokens: YYYY YY MMMM MMM MM M DD D.
minDate | 'today' | stringnullEarliest selectable date.
maxDate | 'today' | stringnullLatest selectable date.
disabledDates(Date|string)[] | fn[]Array of dates, or fn(date) => boolean returning true to disable.
disabledDaysnumber[][]Disabled weekdays, 0 = Sunday … 6 = Saturday.
rangebooleanfalseRange selection — “start – end” in one input, with hover preview.
rangeSeparatorstring' – 'Text joining start and end in the input value.
panesnumber1Months 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).
presetsboolean | string | arraynullQuick-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).
inlinebooleanfalseAlways-visible calendar. Automatic when the target is not an input.
localestringnullBCP-47 tag (e.g. 'fr'); defaults to the browser locale. All names come from Intl — no locale bundles.
firstDaynumbernull0–6; defaults to the locale’s first weekday.
weekNumbersbooleanfalseISO-8601 week number column.
themestring'auto''auto' | 'light' | 'dark' (see Theming).
stylesbooleantruefalse = headless: no CSS is ever injected (see Theming).
accentstringnullAny CSS color for the accent (selection, today, buttons).
todayButtonbooleantrueShow the footer Today button.
clearButtonbooleantrueShow the footer Clear button.
autoClosebooleantrueClose the popup after picking a date (or a full range).
positionstring'auto''auto' | 'below' | 'above' — popup placement relative to the input.
labelsobjectsee descriptionUI 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.
onSelectfunctionnullfn(value, formatted, picker) — see Events & callbacks.
onOpenfunctionnullfn(picker).
onClosefunctionnullfn(picker).
onClearfunctionnullfn(picker).
onMonthChangefunctionnullfn(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.

Methods

All methods return the instance (chainable) unless noted.

MethodReturnsDescription
open()thisShow the popup (no-op for inline calendars).
close()thisHide the popup; returns focus to the input when focus was inside the panel.
toggle()thisOpen if closed, close if open.
getDate()Date | {start, end} | nullCurrent value — a Date, or {start, end} in range mode.
setDate(value, config?)thisSet 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()thisClear the value (fires onClear / datepicker:clear).
setOptions(partial)thisChange 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()thisRemove the panel and all listeners, and unregister the instance.

Statics & helpers

MemberReturnsDescription
DatePicker.create(target, options?)DatePickerConstructor alias — same as new DatePicker(…).
DatePicker.get(target)DatePicker | nullThe 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?)stringPure 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 | nullPure helper — parse a string in the token format; null for invalid or impossible dates (e.g. 2026-02-30).
DatePicker.defaultsobjectGlobal defaults — mutate once to affect every picker created afterwards.
DatePicker.saltstring | falseCSS isolation namespace (default 'vc1'). Set your own token before the first picker, or false to disable salting.
DatePicker.cssstringThe full stylesheet, always rendered with the current salt — a starting point for headless styling.
DatePicker.versionstringLibrary version.

Events & callbacks

Option callbacks:

CallbackSignatureFires
onSelectfn(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.
onOpenfn(picker)When the popup opens.
onClosefn(picker)When the popup closes.
onClearfn(picker)When the value is cleared.
onMonthChangefn(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):

Eventevent.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))

Declarative init

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:

AttributeOptionNotes
data-datepicker—Marks the element for auto-init.
data-valuevalueString in format, ISO, or today.
data-formatformate.g. DD/MM/YYYY.
data-min / data-maxmin / maxDate string or today.
data-disabled-daysdisabledDaysComma list, e.g. "0,6".
data-disabled-datesdisabledDatesComma list, e.g. "2026-12-25,2026-12-26".
data-rangerangeBoolean flag; bare attribute = true, "false"/"0" = false.
data-range-separatorrangeSeparator
data-panespanesNumber 1–3.
data-presetspresetsBare attribute = built-ins; or a comma list of keys, e.g. "last7,thisMonth".
data-inlineinlineBoolean flag.
data-localelocaleBCP-47 tag.
data-first-dayfirstDayNumber 0–6.
data-week-numbersweekNumbersBoolean flag.
data-themethemeauto / light / dark.
data-stylesstylesdata-styles="false" for headless.
data-accentaccentAny CSS color.
data-today-button / data-clear-buttontodayButton / clearButtonBoolean flags.
data-auto-closeautoCloseBoolean flag.
data-positionpositionauto / below / above.
<input data-datepicker data-range data-panes="2"
       data-presets="last7,last30,thisMonth,lastMonth">

Theming

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"] { … }):

PropertyRole
--vdp-accentPrimary color: selection, today, buttons.
--vdp-on-accentText on the accent color.
--vdp-accent-softSoft accent fill (in-range days, active preset).
--vdp-accent-mistFaintest accent wash (range hover preview).
--vdp-bgPanel background.
--vdp-surfaceHover fill.
--vdp-textText color.
--vdp-mutedSecondary text (weekday header, week numbers).
--vdp-faintBorders.
--vdp-shadowPanel shadow.
--vdp-radiusPanel corner radius.
--vdp-cellDay cell size.
--vdp-fontUI font stack.
--vdp-display-fontMasthead (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.

Accessibility

KeyAction
↓ (on the input)Open the calendar and move focus into the grid.
← / →Move focus by one day.
↑ / ↓Move focus by one week.
PageUp / PageDownPrevious / next month.
Shift+PageUp / Shift+PageDownPrevious / next year.
Home / EndFirst / last day of the week.
Enter / SpaceSelect the focused day.
EscapeClose and return focus to the input.
TabTabbing 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.

Headless mode

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.

Unstyled (styles: false)

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>

Bring your own design (Tailwind)

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.