nvcalendar
nvcalendar

Calendar, date picker and attribute-driven date visualisation. Zero runtime dependencies, Intl-powered locales, light and dark themes, fully typed.

nvcalendar

npm version npm downloads License: MIT

Calendar, date picker and attribute-driven date visualisation for Nuxt 3/4 and Vue 3.

  • 📅 Calendar — any number of month panes, keyboard navigation, swipe, transitions
  • 🎯 Date picker — single, multiple and range selection, with or without time
  • 🎨 Attributes — highlights, dots, bars, popovers and custom content, driven by data
  • 🔁 Recurring dates — weekdays, days of month, ordinal weekdays, intervals
  • 🌍 Intl-powered — month/weekday names, first day of week and input masks come from the locale
  • 🌗 Light, dark and system themes, 18 accent presets or any CSS colour
  • ♿ Accessible — grid semantics, roving tab index, arrow-key navigation, focus-visible rings
  • 📦 Zero runtime dependencies, fully typed, SSR-safe

Installation

npx nuxi module add nvcalendar

or manually:

npm install nvcalendar
# pnpm add nvcalendar · yarn add nvcalendar · bun add nvcalendar
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nvcalendar'],
})

That's it — components are auto-imported and the stylesheet is registered for you.

<template>
  <NvDatePicker v-model="date" />
</template>

<script setup lang="ts">
const date = ref(new Date())
</script>

Plain Vue 3 (no Nuxt)

import { createApp } from 'vue'
import NvCalendar from 'nvcalendar/vue'
import 'nvcalendar/styles'
import App from './App.vue'

createApp(App).use(NvCalendar, { color: 'indigo', isDark: 'system' })

Or import the components directly:

import { NvCalendar, NvDatePicker } from 'nvcalendar/vue'

Module options

export default defineNuxtConfig({
  modules: ['nvcalendar'],
  nvcalendar: {
    // Component name prefix: <NvCalendar>, <NvDatePicker>, …
    prefix: 'Nv',
    // Register the bundled stylesheet
    css: true,
    // Auto-import composables
    composables: true,
    // Applied to every component unless overridden by a prop
    defaults: {
      color: 'blue',
      isDark: 'system',
      locale: 'de-DE',
      firstDayOfWeek: 1,
      trimWeeks: true,
      titlePosition: 'center',
      transition: 'slide-h',
      popoverDelay: 120,
      masks: { title: 'MMMM YYYY' },
    },
  },
})

<NvCalendar>

Props

PropTypeDefaultDescription
rowsnumber1Month panes stacked vertically
columnsnumber1Month panes side by side
stepnumberpane countMonths moved per navigation step
page{ month, year }—Controlled page, use with v-model:page
initialPage{ month, year }—Page shown on first render
initialDateDate | string | numbertodayDerives the initial page
minDate / maxDateDate | string | numbernullSelectable and navigable bounds
disabledDatesdate input—Dates that cannot be picked
availableDatesdate input—Inverse of disabledDates
localestring | LocaleConfigbrowser localeBCP-47 id or full config
firstDayOfWeek0–6from locale0 = Sunday
masksLocaleMasks—Format overrides
todayDate | string | numbernowReference date for "today"
colorstringbluePreset name or any CSS colour
isDarkboolean | 'system'falseTheme
expandedbooleanfalseFill the container width
borderless / transparentbooleanfalseChrome removal
trimWeeksbooleanfalseShow only the weeks in the month
titlePosition'left' | 'center' | 'right'centerHeader title alignment
showWeeknumbersboolean | 'left' | 'right'falseLocale week numbers
showIsoWeeknumbersboolean | 'left' | 'right'falseISO-8601 week numbers
attributesCalendarAttribute[][]See Attributes
navVisibility'click' | 'hover' | 'focus' | 'hidden'clickMonth/year picker trigger
hideArrows / hideHeader / hideWeekdaysbooleanfalseChrome removal
transition'slide-h' | 'slide-v' | 'fade' | 'none'slide-hPage transition
disablePageSwipebooleanfalseTurn off touch swipe
disableKeyboardNavigationbooleanfalseTurn off arrow-key navigation
popoverDelaynumber120Hover popover delay in ms
popoverPlacementPlacementbottomDay popover placement

Events

EventPayload
update:page{ month, year }
did-move{ month, year }[] — every visible page
dayclick(day: CalendarDay, event: MouseEvent)
daymouseenter / daymouseleave(day, MouseEvent)
dayfocusin / dayfocusout(day, FocusEvent)
daykeydown(day, KeyboardEvent)
weeknumberclick(week: number, days: CalendarDay[])

Slots

SlotScope
header-title{ page, title }
header-prev-button / header-next-button{ move, disabled }
weekday{ weekday }
weeknumber{ week }
day-content{ day, attributes }
day-popover{ day, attributes, title, hide }
footer—

Exposed methods

<script setup lang="ts">
const calendar = ref()

// calendar.value.movePrev()
// calendar.value.moveNext()
// calendar.value.moveBy(3)
// calendar.value.moveToPage({ month: 12, year: 2026 })
// calendar.value.moveToDate(new Date(2026, 11, 24))
// calendar.value.focusDay(new Date())
</script>

<template>
  <NvCalendar ref="calendar" />
</template>

<NvDatePicker>

Accepts every <NvCalendar> prop plus the following.

PropTypeDefaultDescription
modelValuesee belownullThe selection
mode'date' | 'dateTime' | 'time'dateClock visibility
selection'single' | 'multiple' | 'range'singleSelection behaviour
allowClearbooleantrueRe-clicking a selected day clears it
keepVisibleOnInputbooleanfalseKeep the popover open after picking
is24hrbooleanfrom localeForce the 24-hour clock
timeAccuracy1 | 2 | 32Hours / + minutes / + seconds
rulesDatePickerRules—Restrict selectable time values
hideTimeHeaderbooleanfalseHide the clock header
minRangeSpan / maxRangeSpannumber—Range length limits, in days
selectAttributeCalendarAttribute—Styling of the selection
dragAttributeCalendarAttribute—Styling of a range being dragged
updateOnInputbooleantrueParse while typing
inputDebouncenumber300Debounce for the above, in ms
popoverPlacementInputPlacementbottom-startPopover placement

Model value shapes

<!-- single -->
<NvDatePicker v-model="date" />                      <!-- Date | null -->

<!-- multiple -->
<NvDatePicker v-model="dates" selection="multiple" /> <!-- Date[] -->

<!-- range -->
<NvDatePicker v-model="range" selection="range" />    <!-- { start, end } -->

Strings and timestamps are accepted as input, and can be emitted back with model modifiers:

<NvDatePicker v-model.string="isoDate" :masks="{ modelValue: 'YYYY-MM-DD' }" />
<NvDatePicker v-model.number="timestamp" />

Attaching to an input

Providing the default slot switches the picker into popover mode.

<NvDatePicker v-model="date">
  <template #default="{ inputValue, inputEvents, togglePopover }">
    <input :value="inputValue" v-on="inputEvents">
    <button @click="togglePopover">📅</button>
  </template>
</NvDatePicker>

For a range, inputValue and inputEvents are split into start and end:

<NvDatePicker v-model="range" selection="range">
  <template #default="{ inputValue, inputEvents }">
    <input :value="inputValue.start" v-on="inputEvents.start">
    <input :value="inputValue.end" v-on="inputEvents.end">
  </template>
</NvDatePicker>

Date and time

<NvDatePicker
  v-model="appointment"
  mode="dateTime"
  :rules="{ hours: { min: 9, max: 18 }, minutes: { interval: 15 } }"
/>

rules accepts a fixed value, a list of values or a { min, max, interval } object for hours, minutes and seconds.

Events

update:modelValue, update:page, did-move, dayclick, drag, popover-show, popover-hide.


Attributes

Attributes attach visual meaning to dates. They are plain objects, so they can come straight from an API response.

<NvCalendar :attributes="attributes" />
const attributes = [
  {
    key: 'vacation',
    highlight: { color: 'teal', fillMode: 'light' },
    dates: { start: '2026-08-03', end: '2026-08-17' },
    popover: { label: 'Vacation' },
  },
  {
    key: 'standup',
    dot: 'green',
    dates: { repeat: { weekdays: [1, 2, 3, 4, 5] } },
  },
  {
    key: 'payday',
    bar: 'orange',
    dates: { repeat: { days: -1 } },       // last day of every month
    popover: { label: 'Payday' },
  },
  {
    key: 'release',
    highlight: { fillMode: 'outline' },
    dates: { repeat: { ordinalWeekdays: { 2: 4 } } }, // 2nd Thursday
    customData: { id: 42 },
  },
]

dates

ShapeMeaning
Date | string | numberone day
{ start, end }inclusive range; either side may be null for an open range
{ start, span }range of span days
{ repeat: {...} }recurrence
Array<any of the above>union

excludeDates uses the same shapes and subtracts from the result.

repeat

KeyTypeExample
every'day' | 'week' | 'month' | 'year'every: 'week'
intervalnumberevery 2nd week
weekdays0–6 or array[1, 3, 5]
daysday of month, negative from the end[1, 15, -1]
weeksweek of month, negative from the end-1
months1–12[6, 7, 8]
yearsnumber or array2026
ordinalWeekdays{ ordinal: weekday }{ 1: 1, '-1': 5 }
from / untildatebounds of the recurrence

Decorations

highlight, dot, bar and content accept:

  • true — use the calendar accent colour
  • 'pink' — a preset name, the same palette as the color prop
  • '#ff0055' / 'var(--brand)' — any CSS colour
  • a config object — { color, fillMode, class, style, contentClass, contentStyle }
  • a segmented object — { start, base, end } to style range edges differently

fillMode is 'solid', 'light', 'outline' or 'none'. Ranges default to a solid circle on the first and last day and a light bar in between.

The label colour follows the fill it sits on: the day publishes the colour of its topmost highlight as --nv-accent, and the text is derived from that. On a solid fill the browser picks black or white by the lightness of the colour, so pale accents stay readable.

Popovers

{ popover: 'Simple label' }
{ popover: { label: 'Detailed', visibility: 'click', hideIndicator: true } }

visibility is 'hover', 'focus', 'click', 'visible' or 'hidden'. Use the day-popover slot for full control over the content.


Theming

Every colour is derived from a single custom property, so a theme is one line:

.my-calendar {
  --nv-accent: #7c3aed;
}
<NvCalendar color="#7c3aed" />
<NvCalendar color="var(--brand)" />
<NvCalendar color="violet" />

Presets: gray, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose.

Useful custom properties:

PropertyPurpose
--nv-accentAccent colour, everything else is derived from it
--nv-color-<preset>The 18 palette entries, e.g. --nv-color-teal
--nv-bg, --nv-fg, --nv-fg-muted, --nv-borderSurfaces
--nv-radius, --nv-radius-smCorner rounding
--nv-day-content-size, --nv-day-min-heightDay cell metrics
--nv-pane-padding, --nv-pane-min-widthPane metrics
--nv-font-size, --nv-font-familyTypography
--nv-durationAnimation speed

Dark mode is applied through the nv-dark class, controlled by isDark. isDark="system" follows prefers-color-scheme reactively.


Format tokens

Used by masks and by locale.format().

TokenOutput
YYYY YY2026 26
MMMM MMM MM MJuly Jul 07 7
DD D05 5
dddd ddd dd dSunday Sun S 0
HH H hh h24- and 12-hour
mm m ss s SSSminutes, seconds, milliseconds
A aPM pm
Z ZZ+02:00 +0200
[text]literal text

Default masks:

{
  title: 'MMMM YYYY',
  weekdays: 'dd',
  navMonths: 'MMM',
  dayPopover: 'dddd, D MMMM YYYY',
  input: [/* derived from the locale */, 'YYYY-MM-DD', 'YYYY/MM/DD'],
  modelValue: 'YYYY-MM-DD',
}

Keyboard

KeyAction
← →Previous / next day
↑ ↓Previous / next week
Home EndFirst / last day of the week
PageUp PageDownPrevious / next month
Shift + PageUp/DownPrevious / next year
Enter SpaceSelect the focused day
EscClose a popover, cancel a range drag

Composables and utilities

import {
  useCalendar,          // the state machine behind <NvCalendar>
  useCalendarDefaults,  // read the app-wide defaults
} from '#imports'

import {
  resolveLocale,
  buildPage,
  normalizeDates,
  matchRange,
  addDays,
  diffInDays,
  getIsoWeeknumber,
} from 'nvcalendar/runtime/utils/date'

Types are exported from nvcalendar/types:

import type {
  CalendarAttribute,
  CalendarDay,
  CalendarPage,
  DateRangeValue,
  RepeatRule,
} from 'nvcalendar/types'

SSR

All components render on the server. Popovers are teleported to <body> and mount only on the client, so there is nothing to wrap in <ClientOnly>. Dates are handled in the user's local time zone; pass today if you need a deterministic reference date for snapshot tests.


Development

npm install
npm run dev          # documentation site on http://localhost:3000
npm run test         # unit tests
npm run lint
npm run build        # build the module into dist/

The playground/ directory is the documentation site: a landing page, guides, a generated API reference and live demos, in English and Russian. It is built with Nuxt and Tailwind; the module itself has no dependency on either.

npm run docs:dev       # develop the site
npm run docs:build     # server build, output in playground/.output
npm run docs:generate  # static build, output in playground/.output/public
npm run docs:preview   # preview the production build

Deploy the server build to any Node host, or point a static host at the generated output. The language and theme are stored in cookies and resolved on the server, so a server deployment renders the visitor's language on the first request. A fully static build serves English until hydration.

The API reference is generated from src/runtime/props.ts: prop names, types, defaults and the JSDoc above each prop. Adding a documented prop is enough to make it appear. Demo snippets are extracted from the page sources at runtime, so no example in the site can drift from the code that renders it.


Contributing

Development setup, project layout and the checks a pull request has to pass are in CONTRIBUTING.md. Please read the code of conduct first, and report vulnerabilities privately as described in SECURITY.md.


Credits and licence

nvcalendar is an independent, clean-room implementation released under the MIT licence. It is inspired by the idea of an attribute-driven Vue calendar — an idea popularised by the excellent V-Calendar project — but shares no source code, markup, stylesheets or documentation with it. It is not affiliated with, endorsed by or derived from V-Calendar or its authors. All APIs, class names, CSS and internals here were written from scratch for this package.

If you are looking for the original project, please support it directly.