JavaScript Calendar

@lemonadejs/calendar · ✓ 47 contract checks · framework-agnostic · zero dependencies

<Calendar /> — date, datetime and range picker on the Modal primitive.

v5 parity, the full nuance set:

  • value shapes: ‘YYYY-MM-DD’, ‘YYYY-MM-DD HH:MM:SS’ (time), Excel serial numbers (numeric — 1900 leap-bug compatible), arrays or comma strings (range), Date instances accepted on the way in
  • three views: 42-cell day grid (grey out-of-month days), 12-month picker, 16-year pages; month/year header buttons drill, selecting a month/year returns to the day grid
  • range: first click starts, second ends (clicking at/before the start restarts), live mouseover preview, commit via Done/Update
  • time: hour/minute selects; a day click commits but keeps the panel open, Update closes (v5)
  • min/max ISO bounds + validate(day, month, year, cell) hook (v5 validRange array/function split into three clean props)
  • input mode: the calendar owns its input; typing is masked per format (inlined jSuites date tokens) and steers the view live — commit happens on Enter/click/Done only; the popup is a Modal anchored beneath the input (anchor measured at open)
  • input adoption: input accepts an EXISTING HTMLInputElement (v5 input; v5’s ‘auto’ is simply v6’s default internal input) — the calendar renders no internal input and drives the host’s element instead: value kept formatted per format (an initial input value seeds an empty calendar), open on focus/click, masked typing steers the view, keyboard, and a bubbling change event on every commit; every listener goes through listen() so unmount removes them all
  • initinput=false (v5 initInput): the interactive listeners are NOT wired on the input (adopted or internal — v5 applied it to whichever input the option configured): no open-on-focus/click, no type-to-update, no input keyboard; the input text still tracks the committed value
  • types: default (anchored panel) | picker (bottom sheet) | inline (no modal, always visible) | auto (viewport width at open)
  • keyboard: closed Enter/arrows open; input arrows focus the grid; grid arrows move the cursor (7/4 vertical jump), wrapping across months/years/pages; Enter selects; Escape CANCELS (uncommitted cursor/range/typed text revert) — like every v6 overlay block
  • wheel month navigation (wheel=false opts out), event markers from data=[{date}], today bold, starting weekday (live), grid lines, footer toggle, disabled, placeholder
  • localization through document.dictionary (month/weekday names and the Reset/Done/Update labels), v5’s T()/Helpers hooks

Events: onchange(value), onupdate(cursorIso) on every cursor move, onopen(), onclose(origin: ‘button’ | ‘escape’ | ‘focusout’).

Example

live
import { html } from 'lemonadejs';
import Calendar from '@lemonadejs/calendar';

const App = (props, { state }) => {
    const checkin = state('2026-09-18');
    const stay = state(['2026-10-05', '2026-10-09']);

    return html`<div style="display:grid;gap:12px;max-width:320px">
        <label>Check-in date
            <${Calendar} bind="${checkin}" format="DD/MM/YYYY" placeholder="DD/MM/YYYY"
                min="2026-09-01" startingday="1" />
        </label>
        <label>Conference stay (range, click start and end)
            <${Calendar} bind="${stay}" range placeholder="Pick a range"
                data="${[{ date: '2026-10-07', title: 'Keynote' }]}" />
        </label>
        <p>Check-in on <b>${checkin}</b>, conference from
            <b>${() => (stay.value || []).join(' to ')}</b></p>
    </div>`;
};

Installation

npm install @lemonadejs/calendar
import Calendar from '@lemonadejs/calendar';
import '@lemonadejs/calendar/style.css';

Three deployment forms, one component:

html`<${Calendar} />`                       // by value (no registration)
setComponents({ Calendar });               // then <Calendar /> by name anywhere
createWebComponent(Calendar);              // <lm-calendar> in plain HTML/any framework

Props

Every declared prop arrives as a live state — pass a value for a snapshot or a state for a two-way live wire. Attribute strings are coerced to the declared type.

PropTypeDefaultDescription
bindstringTwo-way bound value. .set() fires onchange; plain assignment is silent. selected value (v5: value) — see formats above
namestring''form field name — when set, the root reflects
rangebooleanfalsetwo-click range selection
timebooleanfalsehour/minute picker, value carries time
numericbooleanfalsevalue as Excel serial number(s)
formatstring''input display/typing mask (default YYYY-MM-DD)
typestring''
dataarray[{ date: ‘YYYY-MM-DD’, … }] event markers
minstring''first selectable date (v5 validRange[0])
maxstring''last selectable date (v5 validRange[1])
validatefunction(day, month, year, cell) -> disabled (v5 validRange fn)
startingdaynumber0first weekday: 0 Sunday .. 6 Saturday (live)
disabledbooleanfalseblocks selection, dims the grid
gridbooleanfalsegrid-line styling (data-grid)
footerbooleantrueUpdate button / time row
wheelbooleantruemouse wheel month navigation
placeholderstring''input placeholder
aria-labelstring''
inputany‘any’: an existing HTMLInputElement to adopt
initinputbooleantruewire the interactive input listeners: open on
widthnumber300popup panel width (v5 modal width)

Events

All event names are lowercase (the platform convention — LJS-305 warns otherwise).

  • onchange — (value) on commit
  • onupdate — (cursorIso) on every cursor move
  • onopen
  • onclose — (origin: ‘button’ | ‘escape’ | ‘focusout’)

API

import { ref } from 'lemonadejs';
const calendar = ref();
html`<${Calendar} ref="${calendar}" />`;
// calendar.current.open(...)  ·  calendar.current.close(...)  ·  calendar.current.isClosed(...)  ·  calendar.current.getValue(...)  ·  calendar.current.setValue(...)  ·  calendar.current.update(...)  ·  calendar.current.reset(...)  ·  calendar.current.next(...)  ·  calendar.current.prev(...)  ·  calendar.current.setView(...)
  • open()
  • close()
  • isClosed()
  • getValue()
  • setValue()
  • update()
  • reset()
  • next()
  • prev()
  • setView()

Styling

All classes follow the lm-calendar-* convention; visual variants are data-* attributes on the root. Override freely — there is no styling engine to fight.

Contract

The machine-readable schema ships with the package:

import contract from '@lemonadejs/calendar/contract.json';

verify.json carries the conformance proof produced by verify(Calendar).

Looking for the v5 plugin? See the archived v5 documentation.