Qure UI
Components

Calendar

A month grid for choosing a day or a span of days.

August 2026
MondayTuesdayWednesdayThursdayFridaySaturdaySunday

Fri Aug 14 2026

'use client'import * as React from 'react'import { Calendar } from '@/registry/qure/ui/calendar'export default function CalendarDemo() {  const [value, setValue] = React.useState<Date | null>(new Date())  return (    <div className="flex flex-col items-center gap-3">      <Calendar        className="border"        value={value}        onValueChange={(v) => setValue(v as Date | null)}      />      <p className="text-label-base text-muted-foreground">        {value ? value.toDateString() : 'Nothing selected'}      </p>    </div>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/calendar.json

Usage

import { Calendar, type DateRange } from "@/components/ui/calendar"
const [value, setValue] = React.useState<Date | null>(new Date())

<Calendar value={value} onValueChange={(v) => setValue(v as Date | null)} />

Uncontrolled works too — pass defaultValue and read the date from onValueChange.

No date library. This uses Date for arithmetic and Intl for names. A component you copy into your repository should not decide that your project now depends on date-fns, and month grids need very little that the platform does not already have.

A range

Pass mode="range" and the value becomes { from, to }. The first click sets from, the second closes the range, and a third starts again. Clicking before the start swaps the ends rather than refusing.

August 2026
MondayTuesdayWednesdayThursdayFridaySaturdaySunday

Fri Aug 14 2026 → Thu Aug 20 2026

'use client'import * as React from 'react'import { Calendar, type DateRange } from '@/registry/qure/ui/calendar'export default function CalendarRange() {  const today = new Date()  const [value, setValue] = React.useState<DateRange | null>({    from: today,    to: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 6),  })  return (    <div className="flex flex-col items-center gap-3">      <Calendar        mode="range"        className="border"        value={value}        onValueChange={(v) => setValue(v as DateRange | null)}      />      <p className="text-label-base text-muted-foreground">        {value?.from          ? `${value.from.toDateString()} → ${value.to ? value.to.toDateString() : 'pick an end'}`          : 'Nothing selected'}      </p>    </div>  )}

While a range is open, hovering previews where it would end. The preview is deliberately invisible to assistive technology — only the two dates actually chosen carry aria-selected, because a hover is not a choice.

Limiting what can be picked

August 2026
MondayTuesdayWednesdayThursdayFridaySaturdaySunday

Weekdays only, within 60 days.

'use client'import * as React from 'react'import { Calendar } from '@/registry/qure/ui/calendar'export default function CalendarBounded() {  const today = new Date()  const [value, setValue] = React.useState<Date | null>(null)  return (    <div className="flex flex-col items-center gap-3">      {/* An appointment cannot be booked in the past, more than 60 days out,          or at a weekend. All three are the same kind of rule, so they are          expressed the same way. */}      <Calendar        className="border"        value={value}        onValueChange={(v) => setValue(v as Date | null)}        min={today}        max={new Date(today.getFullYear(), today.getMonth(), today.getDate() + 60)}        disabled={(date) => date.getDay() === 0 || date.getDay() === 6}      />      <p className="text-label-base text-muted-foreground">        Weekdays only, within 60 days.      </p>    </div>  )}

min and max bound the range; disabled takes a predicate for everything else. All three produce a genuinely disabled button, so keyboard users cannot land on a day the mouse cannot reach either.

<Calendar
  min={today}
  max={addDays(today, 60)}
  disabled={(date) => date.getDay() === 0 || date.getDay() === 6}
/>

Keyboard

The grid is one tab stop, not forty-two — only the focused day is tabbable, so tabbing past a calendar takes a single press.

KeyMoves
a day
a week
Home Endto the start or end of the week
Page Up Page Downa month
Shift + Page Up / Page Downa year
Enter Spacechoose the focused day

Moving off the edge of the month turns the page, and focus follows. Focus is kept separate from selection throughout, so a reader can move across the grid reading dates without choosing one.

Page Up from the 31st lands on the 30th of the month before, not on the 1st of the month before that. Naive month arithmetic overflows — new Date(2025, 2, 31) for February is the 3rd of March — and a date picker that skips a month when you use it on the wrong day is a bug nobody reports and everybody works around.

Localisation

Weekday and month names come from Intl and follow the browser unless you pass locale. The week starts on Monday; pass weekStartsOn={0} for Sunday.

<Calendar locale="en-GB" weekStartsOn={1} />

Each day's accessible name is the fully written date — "Monday 12 May 2025" — rather than the number in the cell, which on its own tells a screen reader user nothing about which month or weekday they have landed on.

Accessibility

The grid is a real role="grid" with column headers, so it is navigable as a table. The three-letter weekday heading is shown and the full name is read.

Today carries aria-current="date" and is marked with a ring rather than a fill, so it cannot be mistaken for the selection. The month caption is aria-live="polite" — the month changing is worth hearing after the day you arrowed onto, not instead of it.

API Reference

Prop

Type

On this page