npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@reachweb/alpine-calendar

v0.6.1

Published

A lightweight, AlpineJS-native calendar component with inline/popup display, input binding with masking, and CSS custom property theming.

Readme

Alpine Calendar

A lightweight, AlpineJS-native calendar component with inline/popup display, input binding with masking, single/multiple/range selection, month/year pickers, birth-date wizard, CSS custom property theming, and timezone-safe date handling.

Live Demo

Installation

npm / pnpm

pnpm add @reachweb/alpine-calendar
# or
npm install @reachweb/alpine-calendar
import Alpine from 'alpinejs'
import { calendarPlugin } from '@reachweb/alpine-calendar'
import '@reachweb/alpine-calendar/css'

Alpine.plugin(calendarPlugin)
Alpine.start()

CDN (no bundler)

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@reachweb/alpine-calendar/dist/alpine-calendar.css">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@reachweb/alpine-calendar/dist/alpine-calendar.cdn.js"></script>

The CDN build auto-registers via alpine:init — no manual setup needed. Works with Livewire, Statamic, or any server-rendered HTML.

Quick Start

You can use x-data in any block element to load the calendar.

Inline Single Date

<div x-data="calendar({ mode: 'single', firstDay: 1 })"></div>

Popup with Input

<div x-data="calendar({ mode: 'single', display: 'popup' })">
  <input x-ref="rc-input" type="text" class="rc-input">
</div>

Provide your own <input> with x-ref="rc-input" — the calendar binds to it automatically, attaching focus/blur handlers, input masking, and ARIA attributes. The popup overlay with close button, transitions, and mobile-responsive sizing is auto-rendered alongside the input.

To use a custom ref name:

<div x-data="calendar({ display: 'popup', inputRef: 'dateField' })">
  <input x-ref="dateField" type="text" class="my-custom-input">
</div>

Range Selection (2-Month)

<div x-data="calendar({ mode: 'range', months: 2, firstDay: 1 })"></div>

Multiple Date Selection

<div x-data="calendar({ mode: 'multiple' })"></div>

Birth Date Wizard

<div x-data="calendar({ mode: 'single', wizard: true })"></div>

Wizard modes: true (or 'full') for Year → Month → Day, 'year-month' for Year → Month, 'month-day' for Month → Day.

Form Submission

<form>
  <div x-data="calendar({ mode: 'single', name: 'date' })"></div>
  <button type="submit">Submit</button>
</form>

When name is set, hidden <input> elements are auto-generated for form submission.

Disabling Auto-Rendering

Set template: false to require a manual template, or provide your own .rc-calendar element — the calendar skips auto-rendering when it detects an existing .rc-calendar:

<!-- Manual template (auto-rendering skipped) -->
<div x-data="calendar({ mode: 'single' })">
  <div class="rc-calendar" @keydown="handleKeydown($event)" tabindex="0" role="application">
    <!-- your custom template here -->
  </div>
</div>

<!-- Explicitly disabled -->
<div x-data="calendar({ mode: 'single', template: false })"></div>

Presetting Values

Initial Value

Set value in the config to pre-select dates on load:

<!-- Single date -->
<div x-data="calendar({ mode: 'single', value: '2026-03-15' })"></div>

<!-- Range -->
<div x-data="calendar({ mode: 'range', value: '2026-03-10 - 2026-03-20' })"></div>

<!-- Multiple dates -->
<div x-data="calendar({ mode: 'multiple', value: '2026-03-10, 2026-03-15, 2026-03-20' })"></div>

Dynamic Updates

Use setValue() to change the selection after initialization:

<div x-data="calendar({ mode: 'single' })" x-ref="cal">
  <button @click="$refs.cal.setValue('2026-06-15')">Set June 15</button>
  <button @click="$refs.cal.clear()">Clear</button>
</div>

Server-Rendered / Livewire

Pass backend variables directly into the config:

<div x-data="calendar({ mode: 'single', value: '{{ $date }}' })"></div>

Or with Livewire's @entangle:

<div x-data="calendar({ mode: 'single', value: @entangle('date') })"></div>

Configuration

All options are passed via x-data="calendar({ ... })".

| Option | Type | Default | Description | |--------|------|---------|-------------| | mode | 'single' \| 'multiple' \| 'range' | 'single' | Selection mode | | display | 'inline' \| 'popup' | 'inline' | Inline calendar or popup with input | | format | string | 'DD/MM/YYYY' | Date format (tokens: DD, MM, YYYY, D, M, YY) | | months | number | 1 | Months to display (1=single, 2=dual side-by-side, 3+=scrollable) | | mobileMonths | number | — | Months to show on mobile (<640px). Only used when months is 2. | | firstDay | 0–6 | 1 | First day of week (0=Sun, 1=Mon, ...) | | mask | boolean | true | Enable input masking | | value | string | — | Initial value (ISO or formatted string) | | name | string | '' | Input name attribute for form submission | | locale | string | — | BCP 47 locale for month/day names | | timezone | string | — | IANA timezone for resolving "today" | | closeOnSelect | boolean | true | Close popup after selection | | wizard | boolean \| 'year-month' \| 'month-day' | false | Birth date wizard mode | | beforeSelect | (date, ctx) => boolean | — | Custom validation before selection | | showWeekNumbers | boolean | false | Show ISO 8601 week numbers alongside the day grid | | inputId | string | — | ID for the popup input (allows external <label for="...">) | | inputRef | string | 'rc-input' | Alpine x-ref name for the input element | | scrollHeight | number | 400 | Max height (px) of scrollable container when months >= 3 | | presets | RangePreset[] | — | Predefined date range shortcuts (see Range Presets) | | constraintMessages | ConstraintMessages | — | Custom tooltip strings for disabled dates | | dateMetadata | DateMetaProvider | — | Per-date metadata: labels, availability, colors (see Date Metadata) | | template | boolean | true | Auto-render template when no .rc-calendar exists |

Date Constraints

| Option | Type | Description | |--------|------|-------------| | minDate | string | Earliest selectable date (ISO) | | maxDate | string | Latest selectable date (ISO) | | disabledDates | string[] | Specific dates to disable (ISO) | | disabledDaysOfWeek | number[] | Days of week to disable (0=Sun, 6=Sat) | | enabledDates | string[] | Force-enable specific dates (overrides day-of-week rules) | | enabledDaysOfWeek | number[] | Only these days are selectable | | disabledMonths | number[] | Months to disable (1=Jan, 12=Dec) | | enabledMonths | number[] | Only these months are selectable | | disabledYears | number[] | Specific years to disable | | enabledYears | number[] | Only these years are selectable | | minRange | number | Minimum range length in days (inclusive) | | maxRange | number | Maximum range length in days (inclusive) | | rules | CalendarConfigRule[] | Period-specific constraint overrides |

Period-Specific Rules

Override constraints for specific date ranges. First matching rule wins; unmatched dates use global constraints.

<div x-data="calendar({
  mode: 'range',
  minRange: 3,
  rules: [
    {
      from: '2025-06-01',
      to: '2025-08-31',
      minRange: 7,
      disabledDaysOfWeek: [0, 6]
    }
  ]
})">

Reactive State

These properties are available in templates via Alpine's reactivity:

| Property | Type | Description | |----------|------|-------------| | mode | string | Current selection mode | | display | string | 'inline' or 'popup' | | month | number | Currently viewed month (1–12) | | year | number | Currently viewed year | | view | string | Current view: 'days', 'months', or 'years' | | isOpen | boolean | Whether popup is open | | grid | MonthGrid[] | Day grid data for rendering | | monthGrid | MonthCell[][] | Month picker grid | | yearGrid | YearCell[][] | Year picker grid | | inputValue | string | Formatted selected value | | focusedDate | CalendarDate \| null | Keyboard-focused date | | hoverDate | CalendarDate \| null | Mouse-hovered date (for range preview) | | wizardStep | number | Current wizard step (0=off, 1–3) | | showWeekNumbers | boolean | Whether week numbers are displayed | | presets | RangePreset[] | Configured range presets | | isScrollable | boolean | Whether the calendar uses scrollable layout (months >= 3) |

Computed Getters

| Getter | Type | Description | |--------|------|-------------| | selectedDates | CalendarDate[] | Array of selected dates | | formattedValue | string | Formatted display string | | hiddenInputValues | string[] | ISO strings for hidden form inputs | | focusedDateISO | string | ISO string of focused date (for aria-activedescendant) | | weekdayHeaders | string[] | Localized weekday abbreviations | | yearLabel | string | Current year as string | | decadeLabel | string | Decade range label (e.g., "2024 – 2035") | | wizardStepLabel | string | Current wizard step name | | canGoPrev | boolean | Whether backward navigation is possible | | canGoNext | boolean | Whether forward navigation is possible |

Methods

Navigation

| Method | Description | |--------|-------------| | prev() | Navigate to previous month/year/decade | | next() | Navigate to next month/year/decade | | goToToday() | Jump to current month | | goTo(year, month?) | Navigate to specific year/month | | setView(view) | Switch to 'days', 'months', or 'years' |

Selection

| Method | Description | |--------|-------------| | selectDate(date) | Select or toggle a date | | selectMonth(month) | Select month in month picker | | selectYear(year) | Select year in year picker | | clearSelection() | Clear all selected dates | | isSelected(date) | Check if date is selected | | isInRange(date, hover?) | Check if date is within range | | isRangeStart(date) | Check if date is range start | | isRangeEnd(date) | Check if date is range end | | applyPreset(index) | Apply a range preset by index |

Programmatic Control

Access these via $refs:

<div x-data="calendar({ ... })" x-ref="cal">
  <button @click="$refs.cal.setValue('2025-06-15')">Set Date</button>
  <button @click="$refs.cal.clear()">Clear</button>
</div>

| Method | Description | |--------|-------------| | setValue(value) | Set selection (ISO string, string[], or CalendarDate) | | clear() | Clear selection | | goTo(year, month) | Navigate without changing selection | | open() / close() / toggle() | Popup lifecycle | | getSelection() | Get current selection as CalendarDate[] | | updateConstraints(options) | Update constraints at runtime | | updateDateMetadata(provider) | Replace metadata at runtime (static map, callback, or null to clear) |

Template Helpers

| Method | Description | |--------|-------------| | dayClasses(cell) | CSS class object for day cells | | dayMeta(cell) | Get DateMeta for a day cell (label, availability, color, cssClass) | | dayStyle(cell) | Inline style string for metadata color (--color-calendar-day-meta) | | monthClasses(cell) | CSS class object for month cells | | yearClasses(cell) | CSS class object for year cells | | monthYearLabel(index) | Formatted "Month Year" label for grid at index | | handleKeydown(event) | Keyboard navigation handler | | handleFocus() | Input focus handler (opens popup) | | handleBlur() | Input blur handler (parses typed value) |

Input Binding

| Method | Description | |--------|-------------| | bindInput(el) | Manually bind to an input element | | handleInput(event) | For unbound inputs using :value + @input |

Events

Listen with Alpine's @ syntax on the calendar container:

<div x-data="calendar({ ... })"
     @calendar:change="console.log($event.detail)"
     @calendar:navigate="console.log($event.detail)">

| Event | Detail | Description | |-------|--------|-------------| | calendar:change | { value, dates, formatted } | Selection changed | | calendar:navigate | { year, month, view } | Month/year navigation | | calendar:open | — | Popup opened | | calendar:close | — | Popup closed | | calendar:view-change | { view, year, month } | View switched (days/months/years) |

Keyboard Navigation

| Key | Action | |-----|--------| | Arrow keys | Move focus between days | | Enter / Space | Select focused day | | Page Down / Up | Next / previous month | | Shift + Page Down / Up | Next / previous year | | Home / End | First / last day of month | | Escape | Close popup or return to day view |

Theming

The calendar uses CSS custom properties for all visual styles. Override them in your CSS:

Override variables

:root {
  --color-calendar-primary: #4f46e5;
  --color-calendar-primary-text: #ffffff;
  --color-calendar-bg: #ffffff;
  --color-calendar-text: #111827;
  --color-calendar-hover: #f3f4f6;
  --color-calendar-range: #eef2ff;
  --color-calendar-today-ring: #818cf8;
  --color-calendar-disabled: #d1d5db;
  --color-calendar-border: #e5e7eb;
  --color-calendar-other-month: #9ca3af;
  --color-calendar-weekday: #6b7280;
  --color-calendar-focus-ring: #4f46e5;
  --color-calendar-overlay: rgba(0, 0, 0, 0.2);
  --radius-calendar: 0.5rem;
  --shadow-calendar: 0 10px 15px -3px rgb(0 0 0 / 0.1);
  --font-calendar: system-ui, -apple-system, sans-serif;
}

CSS Class Reference

All classes use the .rc- prefix:

| Class | Description | |-------|-------------| | .rc-calendar | Root container | | .rc-header / .rc-header__nav / .rc-header__label | Navigation header | | .rc-weekdays / .rc-weekday | Weekday header row | | .rc-grid | Day grid container | | .rc-day | Day cell | | .rc-day--today | Today's date | | .rc-day--selected | Selected date | | .rc-day--range-start / .rc-day--range-end | Range endpoints | | .rc-day--in-range | Dates within range | | .rc-day--disabled | Disabled date | | .rc-day--other-month | Leading/trailing days | | .rc-day--focused | Keyboard-focused date | | .rc-day--available / .rc-day--unavailable | Metadata availability states | | .rc-day--has-label | Day cell with a metadata label | | .rc-day__number / .rc-day__label / .rc-day__dot | Day cell inner elements (number, label text, availability dot) | | .rc-month-grid / .rc-month | Month picker | | .rc-year-grid / .rc-year | Year picker | | .rc-months--dual | Two-month side-by-side layout | | .rc-nav--dual-hidden | Hidden nav arrow in dual-month layout (prev on 2nd month) | | .rc-nav--dual-next-first | Next arrow on 1st month (hidden on desktop, visible on mobile) | | .rc-nav--dual-next-last | Next arrow on 2nd month (visible on desktop, hidden on mobile) | | .rc-popup-overlay | Popup backdrop | | .rc-popup-header / .rc-popup-header__close | Popup close header bar | | .rc-calendar--wizard | Wizard mode container | | .rc-row--week-numbers / .rc-week-number | Week number row and cell | | .rc-grid--week-numbers | Grid with week number column | | .rc-presets / .rc-preset | Range preset container and buttons | | .rc-months--scroll | Scrollable multi-month container | | .rc-header--scroll-sticky | Sticky header in scrollable layout | | .rc-sr-only | Screen reader only utility |

Global Defaults

Set defaults that apply to every calendar instance:

import { calendarPlugin } from '@reachweb/alpine-calendar'

calendarPlugin.defaults({ firstDay: 1, locale: 'el' })
Alpine.plugin(calendarPlugin)

Instance config overrides global defaults.

Week Numbers

Display ISO 8601 week numbers alongside the day grid:

<div x-data="calendar({ mode: 'single', showWeekNumbers: true, firstDay: 1 })"></div>

Week numbers appear in a narrow column to the left of each row.

Range Presets

Add quick-select buttons for common date ranges. Works with range and single modes:

<div x-data="calendar({
  mode: 'range',
  presets: [
    presetToday(),
    presetLastNDays(7),
    presetThisWeek(),
    presetThisMonth(),
    presetLastMonth()
  ]
})"></div>

Import the built-in factories:

import {
  presetToday,
  presetYesterday,
  presetLastNDays,
  presetThisWeek,
  presetLastWeek,
  presetThisMonth,
  presetLastMonth,
  presetThisYear,
  presetLastYear,
} from '@reachweb/alpine-calendar'

All factories accept an optional label and timezone parameter. presetThisWeek and presetLastWeek also accept a firstDay (default: 1 = Monday).

Custom presets:

const customPreset = {
  label: 'Next 30 Days',
  value: () => {
    const today = CalendarDate.today()
    return [today, today.addDays(29)]
  }
}

Date Metadata

Attach labels, pricing, availability indicators, and custom colors to individual dates. Useful for booking calendars, event schedules, and pricing displays.

Static Map

Pass an object keyed by ISO date strings:

<div x-data="calendar({
  mode: 'single',
  dateMetadata: {
    '2026-03-01': { label: '$120', availability: 'available' },
    '2026-03-05': { label: '$180', availability: 'available', color: '#ea580c' },
    '2026-03-06': { availability: 'unavailable' },
    '2026-03-07': { label: 'Sold', availability: 'unavailable' },
  }
})"></div>

Dynamic Callback

Use a function for computed metadata. Called for each visible date:

<div x-data="calendar({
  mode: 'range',
  dateMetadata: (date) => {
    const d = date.toNativeDate().getDay()
    if (d === 0 || d === 6) return { availability: 'unavailable' }
    return { label: '$' + (100 + date.day * 3), availability: 'available' }
  }
})"></div>

DateMeta Properties

| Property | Type | Description | |----------|------|-------------| | label | string | Text below the day number (e.g., price, event name) | | availability | 'available' \| 'unavailable' | 'available' shows a green dot, 'unavailable' disables selection with strikethrough | | color | string | CSS color for the label and dot (e.g., '#16a34a') | | cssClass | string | Custom CSS class(es) added to the day cell |

All properties are optional and work independently. Dates with availability: 'unavailable' cannot be selected regardless of constraint settings.

Runtime Updates

Replace metadata after initialization with updateDateMetadata():

// Update with new data (e.g., after fetching availability)
$refs.cal.updateDateMetadata({
  '2026-03-15': { label: '$200', availability: 'available' },
  '2026-03-20': { availability: 'unavailable' },
})

// Clear all metadata
$refs.cal.updateDateMetadata(null)

Multi-Month Scrollable Layout

When months is 3 or more, the calendar renders as a vertically scrollable container instead of side-by-side panels:

<div x-data="calendar({ mode: 'range', months: 6 })"></div>

<!-- Custom scroll height -->
<div x-data="calendar({ mode: 'range', months: 12, scrollHeight: 500 })"></div>

A sticky header tracks the currently visible month as you scroll. Default scroll height is 400px.

Responsive Behavior

  • Mobile (<640px): Popup renders as a centered fullscreen overlay. Touch-friendly targets (min 44px).
  • Desktop (>=640px): Popup renders as a centered modal with scale-in animation.
  • Two months: Side-by-side on desktop, stacked on mobile. Both nav arrows appear on the top month when stacked.
  • mobileMonths: Show fewer months on mobile (e.g., mobileMonths: 1 with months: 2 displays a single month on narrow viewports).
  • Scrollable (3+ months): Smooth scroll with -webkit-overflow-scrolling: touch.
  • prefers-reduced-motion: All animations are disabled.

Mobile Months

When using months: 2, the calendar shows two months side-by-side on desktop and stacks them vertically on mobile. Set mobileMonths: 1 to show only a single month on mobile instead:

<div x-data="calendar({ mode: 'range', months: 2, mobileMonths: 1 })"></div>

The calendar listens for viewport changes at the 640px breakpoint and switches between the desktop and mobile month counts automatically. Selection is preserved across viewport changes.

Accessibility

The calendar targets WCAG 2.1 AA compliance:

  • Full keyboard navigation (arrow keys, Enter, Escape, Page Up/Down, Home/End)
  • ARIA roles: application, dialog, combobox, option, group
  • aria-live="polite" announcements for navigation and selection changes
  • aria-activedescendant for focus management within the grid
  • aria-modal="true" on popup overlays
  • aria-expanded, aria-selected, aria-disabled on interactive elements
  • :focus-visible outlines on all interactive elements
  • Screen reader support via .rc-sr-only utility class
  • Validated with axe-core (no critical or serious violations)

Bundle Outputs

| File | Format | Size (gzip) | Use case | |------|--------|-------------|----------| | alpine-calendar.es.js | ESM | ~19KB | Bundler (import) | | alpine-calendar.umd.js | UMD | ~12KB | Legacy (require()) | | alpine-calendar.cdn.js | IIFE | ~12KB | CDN / <script> tag | | alpine-calendar.css | CSS | ~4KB | All environments |

TypeScript

Full type definitions are included. Key exports:

import {
  calendarPlugin,
  CalendarDate,
  getISOWeekNumber,
  SingleSelection,
  MultipleSelection,
  RangeSelection,
  createCalendarData,
  parseDate,
  formatDate,
  createMask,
  computePosition,
  autoUpdate,
  generateMonth,
  generateMonths,
  generateMonthGrid,
  generateYearGrid,
  createDateConstraint,
  createRangeValidator,
  createDisabledReasons,
  isDateDisabled,
  normalizeDateMeta,
  presetToday,
  presetYesterday,
  presetLastNDays,
  presetThisWeek,
  presetLastWeek,
  presetThisMonth,
  presetLastMonth,
  presetThisYear,
  presetLastYear,
} from '@reachweb/alpine-calendar'

import type {
  CalendarConfig,
  CalendarConfigRule,
  RangePreset,
  DayCell,
  MonthCell,
  YearCell,
  Selection,
  Placement,
  PositionOptions,
  DateConstraintOptions,
  DateConstraintProperties,
  DateConstraintRule,
  ConstraintMessages,
  DateMeta,
  DateMetaProvider,
  InputMask,
  MaskEventHandlers,
} from '@reachweb/alpine-calendar'

Livewire Integration

@push('styles')
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@reachweb/alpine-calendar/dist/alpine-calendar.css">
@endpush
@push('scripts')
  <script src="https://cdn.jsdelivr.net/npm/@reachweb/alpine-calendar/dist/alpine-calendar.cdn.js"></script>
@endpush

Use wire:ignore on the calendar container to prevent Livewire from morphing it:

<div wire:ignore>
  <div x-data="calendar({ mode: 'single', display: 'popup' })"
       @calendar:change="$wire.set('date', $event.detail.value)">
    <input x-ref="rc-input" type="text" class="rc-input">
  </div>
</div>

Development

pnpm install          # Install dependencies
pnpm dev              # Start dev server with demo
pnpm test             # Run tests
pnpm test:watch       # Run tests in watch mode
pnpm test:coverage    # Run tests with coverage report
pnpm typecheck        # Type-check without emitting
pnpm lint             # Lint source files
pnpm lint:fix         # Lint and auto-fix
pnpm format           # Format source files with Prettier
pnpm build            # Build all bundles (ESM + UMD + CDN + CSS + types)
pnpm build:lib        # Build ESM + UMD only
pnpm build:cdn        # Build CDN/IIFE bundle only

Before a release, run the full verification chain:

pnpm typecheck && pnpm lint && pnpm test && pnpm build

License

MIT