Skip to content
omaris

Inputs

Calendar

Month grid — one date, a range, or a handful of dates.

import { Calendar } from 'omaris'
Learn

Examples

Basic

One date, bound. The value is a Date at local midnight; the caption opens a month and year grid for jumping further than a few presses.

Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday

Sunday, September 27, 2026

<script lang="ts">	import { Calendar, Text, latn } from 'omaris';​	let day = $state<Date | null>(new Date());​	const long = new Intl.DateTimeFormat('en-US', latn({ dateStyle: 'full' }));</script>​<div class="flex flex-col items-start gap-2">	<Calendar bind:value={day} class="rounded-shape-lg border border-border" />	<Text variant="body-sm" tone="muted">{day ? long.format(day) : 'No date'}</Text></div>

Range

mode="range": the first press starts it, the second ends it, and in between the band follows the pointer or the focus. months={2} puts two side by side from sm up and stacks them below.

Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday

Sep 30 – Oct 5

<script lang="ts">	import { Calendar, Text, addDays, startOfDay, latn, type DateRange } from 'omaris';​	const today = startOfDay(new Date());	let stay = $state<DateRange | null>({ start: addDays(today, 3), end: addDays(today, 8) });​	const short = new Intl.DateTimeFormat('en-US', latn({ month: 'short', day: 'numeric' }));</script>​<div class="flex flex-col items-start gap-2">	<Calendar		mode="range"		months={2}		min={today}		bind:value={stay}		class="rounded-shape-lg border border-border"	/>	<Text variant="body-sm" tone="muted">		{stay ? short.formatRange(stay.start, stay.end) : 'Pick a check-in day'}	</Text></div>

Multiple and bounds

mode="multiple" toggles days in and out of a sorted array. min, max and isDateDisabled take days off the table — here, the next six weeks with Fridays and Saturdays closed — and showWeekNumbers adds the ISO week.

Week
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Week 35
Week 36
Week 37
Week 38
Week 39
Week 40

0 shifts picked

<script lang="ts">	import { Calendar, Text, addDays, startOfDay } from 'omaris';​	const today = startOfDay(new Date());	let shifts = $state<Date[]>([]);​	const closed = (date: Date) => date.getDay() === 5 || date.getDay() === 6;</script>​<div class="flex flex-col items-start gap-2">	<Calendar		mode="multiple"		bind:value={shifts}		min={today}		max={addDays(today, 42)}		isDateDisabled={closed}		showWeekNumbers		class="rounded-shape-lg border border-border"	/>	<Text variant="body-sm" tone="muted">		{shifts.length === 1 ? '1 shift' : `${shifts.length} shifts`} picked	</Text></div>

Locale

Names come from Intl and the first weekday from the locale — Saturday in Iraq, Monday in Germany. Inside dir="rtl" the arrows, the keys and the band all mirror, and the digits stay 0–9.

السبت
الأحد
الاثنين
الثلاثاء
الأربعاء
الخميس
الجمعة
Montag
Dienstag
Mittwoch
Donnerstag
Freitag
Samstag
Sonntag
<script lang="ts">	import { Calendar } from 'omaris';</script>​<div class="flex flex-wrap items-start gap-4">	<div dir="rtl" lang="ar-IQ">		<Calendar locale="ar-IQ" class="rounded-shape-lg border border-border" />	</div>	<Calendar locale="de-DE" weekdayFormat="short" class="rounded-shape-lg border border-border" /></div>

Overridden

class is the root, so a card's padding replaces the calendar's own, and classes reaches every part — here square faces, a tinted caption and a wider cell through the --cal-cell property the grid is sized from.

Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
<script lang="ts">	import { Calendar } from 'omaris';</script>​<div class="w-full max-w-sm rounded-shape-xl bg-surface-container-low p-2">	<Calendar		class="flex w-full p-1 [--cal-cell:3rem]"		aria-label="Delivery day"		classes={{			face: 'rounded-shape-sm',			caption: 'text-primary',			weekday: 'text-label-sm uppercase'		}}	/></div>

When to use it

Use it for

  • A date picked by looking at the month it falls in — a delivery day, a booking, a shift — where seeing the weekday and the week around it is the point.
  • A span of days with mode="range": a stay, a leave request, a report period. months={2} shows a range that crosses a month end without paging.
  • Several separate days with mode="multiple" — rota shifts, class dates, blackout days.
  • A calendar that stays on the page: a booking sidebar, a scheduling panel, the body of a card. For one that opens from a field, reach for the field.

Not for

  • A date inside a form, shown as text until it is edited → Date Field. It wraps this calendar in a popover on a desktop and a sheet on a phone.
  • A period filter on a dashboard, with shortcuts like "Last 30 days" → Date Range Field.
  • A date far from today — a birthday, an expiry year → the DatePicker wheels in Picker. Paging back forty years a month at a time is the wrong tool, even with the year grid.
  • A time of day → TimePicker in Picker.
  • Showing events across a month or a week. This picks dates; it does not lay out a schedule.

Do

  • Keep values at local midnight — new Date(2026, 2, 3), or startOfDay on anything else. The calendar hands back midnights, and compares by day.
  • Set min and max whenever the valid window is known. The arrows stop at its months and the out-of-range days grey out, which is kinder than an error after the fact.
  • Use isDateDisabled for the rules a window cannot express — weekends, holidays, sold-out nights.
  • Leave locale and weekStartsOn alone unless the page's language is wrong for the calendar. Both follow <html lang>, and a week that starts on the wrong day is the first thing people notice.

Don't

  • Build dates from ISO strings — new Date('2026-03-03') is UTC midnight, which is the 2nd of March west of Greenwich.
  • Expect a range's value to change on the first press. It is set once the range is whole; the half-drawn state is the calendar's own.
  • Set showOutsideDays with two months — the neighbouring month is already on screen, so it is ignored.
  • Squeeze it much under 280px. The cells shrink to fit, but below about 36px a day is hard to hit with a finger.

API

Calendar

Month grid — one date, a range, or a handful of dates.

Values are plain Dates at local midnight: new Date(2026, 2, 3) is March 3rd, and so is every date the calendar gives back. What value holds follows mode — a Date (or null) for single, a { start, end } (or null) for range, a Date[] for multiple.

It is the full WAI-ARIA date grid: arrows move a day or a week, Home and End go to the ends of the week, PageUp/PageDown a month, with Shift a year, and only one day is ever a tab stop. The caption opens a month and year grid for jumping further than a few presses. A range previews itself from its first day to wherever the pointer or the focus is, and its band is drawn with logical sides, so it reads right in Arabic.

Names come from Intl in the page's language, with Western digits always; the week starts on the day that language starts it on.

import { Calendar } from 'omaris'
<Calendar bind:value={day} min={today} /><Calendar mode="range" months={2} bind:value={range} />

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
The whole calendar. --cal-cell here sizes every day.
months
The row of months — side by side from sm up, stacked below it.
month
One month: its header and its grid. Hidden under the month/year grid.
header
Caption and the previous/next buttons.
caption
The month and year — a button that opens the month/year grid.
nav
The previous and next buttons.
grid
One month's role="grid".
weeks
The weeks, which slide when the month changes.
row
A week, or the weekday headings — seven columns, eight with week numbers.
weekday
A weekday column heading.
weekNumber
The ISO week number at the start of a row.
cell
The gridcell around a day.
day
The day's button — the full cell, so the hit area is the cell.
track
Holds the band and the face, and is exactly as tall as the face.
band
The range band behind the faces, rounded where a row or the range ends.
face
The round face with the day number on it.
picker
The month or year grid the caption opens, laid over the days.
pickerHeader
The year (or the span of years) and its arrows.
pickerGrid
Three columns of months or years.
pickerItem
A month or a year in that grid.

Props

mode
M

'single' (the default), 'range' or 'multiple'. Decides what value holds.

value bindable
CalendarValue<M>

The selection, at local midnight. A Date or null in single, { start, end } or null in range — set on the second press, never half-filled — and a sorted Date[] in multiple.

month bindable
Date

The first month on screen. Defaults to the selection's month, or today's.

min
Date

Earliest selectable day. The previous button stops at its month.

max
Date

Latest selectable day. The next button stops at its month.

isDateDisabled
(date: Date) => boolean

Days that cannot be picked — weekends, holidays, sold-out dates.

locale
string

BCP-47 tag for names and the first weekday. Defaults to <html lang>, then the browser.

weekStartsOn
Weekday

0 is Sunday. Defaults to the locale's own first day.

months

Defaults to 1

1 | 2

Months side by side. They stack below the sm breakpoint.

showOutsideDays

Defaults to true

boolean

Fill the first and last rows with the neighbouring months' days. Ignored with two months, where the neighbour is already on screen.

showWeekNumbers

Defaults to false

boolean

ISO week numbers at the start of each row.

weekdayFormat

Defaults to 'narrow'

'narrow' | 'short'

Weekday headings: narrow ("M") or short ("Mon").

autofocus

Defaults to false

boolean

Focus the active day as soon as the calendar mounts — for one that opens in a popup.

swipe

Defaults to true

boolean

Swipe sideways on a touch screen to change month.

haptics

Defaults to true

boolean

Vibrate on a pick, where the haptics store is on.

labels
CalendarLabels

Accessible names for the arrows, the caption and the week column — for translation.

onchange
(value: CalendarValue<M>) => void

Fires with the new value after every pick — for range, once the range is whole.

class
string

Merged onto the root, so class="p-0" removes the padding.

classes
CalendarClasses

Per-part Tailwind overrides. class still covers the root.