# Calendar

> A React calendar to pick one day, several days or a range, with disabled days, event dots, several months and a quick month / year jump.

- Type: React component (Inputs)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add calendar`
- Docs: https://wingo-ui.com/components/calendar

## Interaction

Click a day to pick it; in multiple mode each click adds or removes a day, and in range mode the first click sets the start, the band follows the pointer as a preview, and the second click sets the end (a day before the start swaps them). The arrows move a month (or a page with pagedNavigation) and the grid slides the way you went. Click the caption for a months grid, click again for years, pick one to zoom back in. Keyboard: Tab lands on one day (the selected, else today); arrows move a day or a week, Home / End jump to the week's ends, PageUp / PageDown move a month and Shift+PageUp / PageDown a year, Enter or Space picks; in the months and years grids arrows move, Enter picks and Escape returns to the days. On a phone the cells grow to 44px and a swipe left or right changes the month; vertical scrolling still works.

## Installation

```bash
npx wingo-ui@latest add calendar
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/calendar` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/calendar.tsx`

npm dependencies: `npm i date-fns motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { Calendar, type DateRange } from "@/components/ui/calendar"

const booked = [new Date(2026, 9, 9), new Date(2026, 9, 10), new Date(2026, 9, 11)]

export function StayPicker() {
  const [stay, setStay] = useState<DateRange>({ from: null, to: null })
  return (
    <Calendar
      mode="range"
      value={stay}
      onValueChange={setStay}
      min={new Date()}
      disabled={booked}
      minDays={3}
      strikeDisabled
      events={[{ date: new Date(2026, 9, 14), tone: "warning", label: "Request pending" }]}
      labels={{ today: "Back to today", selected: "picked" }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `mode?` | `"single" \| "multiple" \| "range"` | `"single"` | What a click picks: one day, a set of days (each click adds or removes one), or a range (start, then end). The value type follows the mode: Date \| null, Date[] or { from, to }. |
| `value?` | `Date \| null \| Date[] \| DateRange` |  | The selection, controlled. Typed by mode: Date \| null for single, Date[] for multiple, { from: Date \| null; to: Date \| null } for range. A value outside min / max shows as given; only clicks are limited. |
| `defaultValue?` | `Date \| null \| Date[] \| DateRange` |  | The starting selection while uncontrolled: null, [] or { from: null, to: null } by mode. |
| `onValueChange?` | `(value: Date \| null \| Date[] \| DateRange) => void` |  | Called on every pick with the next value (typed by mode). In single mode a new day keeps the time of the previous value, so a date-time picker never loses its time. |
| `month?` | `Date` |  | The first visible month, controlled. Pair it with onMonthChange. |
| `defaultMonth?` | `Date` |  | The first visible month on mount. Defaults to the value's month, else today's, and is clamped to min / max. |
| `onMonthChange?` | `(month: Date) => void` |  | Called with the first day of the new first visible month after an arrow, a swipe, a keyboard move or a jump. |
| `numberOfMonths?` | `number` | `1` | Months side by side from 640px wide; below that they stack, each with its own caption. In a narrow container on a wide screen, stack them with classNames={{ months: "sm:flex-col" }} (or a container query). |
| `pagedNavigation?` | `boolean` | `false` | The arrows and swipes move by numberOfMonths instead of one month. |
| `min?` | `Date` |  | Earlier days are disabled and the previous arrow stops at this month. |
| `max?` | `Date` |  | Later days are disabled and the next arrow stops at this month. min after max leaves nothing enabled (a dev warning says so). |
| `disabled?` | `DateMatcher or DateMatcher[] (Date, Date[], { from, to }, { before }, { after }, { dayOfWeek } or (date) => boolean)` |  | Days that can not be picked: a Date, a list, { from, to }, { before }, { after }, { dayOfWeek: [0, 6] } for weekends, or a (date) => boolean test. Disabled days stay focusable (aria-disabled) so the keyboard can pass over them. |
| `locale?` | `string` | `""` | Month and weekday names, digits and the first day of the week, all from Intl. Empty uses the nearest LocaleProvider, else the page's language. en-US starts on Sunday, de-DE on Monday. |
| `weekStartsOn?` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| -1` | `-1` | The first day of the week: 0 is Sunday, 1 Monday ... 6 Saturday; -1 follows the locale (Sunday in en-US, Monday in de-DE). |
| `showOutsideDays?` | `boolean` | `true` | Shows the neighbour months' days that fill the first and last week, dimmed. They can be picked and move the view there. |
| `fixedWeeks?` | `boolean` | `false` | Always six weeks, so the grid never changes height between months (pickers turn it on). Without it the height change animates. |
| `showWeekNumbers?` | `boolean` | `false` | A column of week numbers: ISO weeks when the week starts on Monday, the locale's numbering otherwise. On touch screens md days take 40px instead of 44px to make room, so it still fits a 360px phone. |
| `events?` | `{ date: Date; tone?: Tone; color?: string; label?: string }[]` |  | Dots under a day (up to 3): bookings, deadlines, holidays. tone or any color; labels are read out with the day. On a selected day the dots turn the selection's text color. |
| `monthYearPicker?` | `boolean` | `true` | The caption becomes a button: first a months grid, then a years grid (20 at a time), for fast jumps to a birthday or next year. Off, the caption is plain text. |
| `minDays?` | `number` | `0` | Range mode: the shortest range in days, both ends counted (a 2 night minimum stay is 3). While picking the end, closer days are unavailable; clicking the start again then clears it. 0 = no minimum. |
| `maxDays?` | `number` | `0` | Range mode: the longest range in days, both ends counted. Farther days are unavailable while picking the end. 0 = no limit. |
| `excludeDisabled?` | `boolean` | `true` | Range mode: a range can not span a disabled day. While picking the end, every day past the first disabled one on each side is unavailable (booked nights in a stay). |
| `maxSelected?` | `number` | `0` | Multiple mode: how many days can be picked. At the limit the other days are unavailable until one is removed. 0 = no limit. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Day cells of 32, 36 or 44px. On touch screens md grows to 44px, so seven columns still fit a 360px phone. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The selection color: the day fill, the range band and the months / years highlight. |
| `color?` | `string` | `""` | Any CSS color for the selection; replaces the tone. Text on the fill turns black or white by lightness and the band is a tint of it. |
| `radius?` | `"md" \| "full"` | `"full"` | The day shape: circles (the default, like iOS) or rounded squares. The range band rounds to match. |
| `strikeDisabled?` | `boolean` | `false` | Strikes through disabled days, for booked nights and sold-out dates. |
| `todayButton?` | `boolean` | `false` | A Today button under the grid that jumps back to the current month (and closes the months / years view). |
| `labels?` | `Partial<{ previousMonth; nextMonth; previousYear; nextYear; previousYears; nextYears; chooseMonth; chooseYear; today; weekNumber; selected; event: string }>` |  | Every visible and spoken string, merged over the English defaults ("Previous month", "Today", "selected"...). Pass translated ones in another language. |
| `renderDay?` | `(date: Date, state: { today; selected; rangeStart; rangeMiddle; rangeEnd; outside; disabled; hoverRange; events }) => ReactNode` |  | The content inside a day button, in place of the number: a price under each day, a badge. Use size lg for two lines. |
| `footer?` | `ReactNode` |  | Content under the grid: a range summary, a time field, actions. |
| `autoFocus?` | `boolean` | `false` | Focuses the selected day (else today) on mount. Pickers pass it when they open from the keyboard. |
| `name?` | `string` | `""` | Renders hidden inputs for native forms, as local ISO dates ("2026-09-23"): one for single, one per day for multiple, name.from and name.to for a range. A form reset returns to defaultValue. The demo prints what the form would submit. |
| `classNames?` | `Partial<Record<"header" \| "caption" \| "nav" \| "months" \| "grid" \| "weekday" \| "day" \| "today" \| "selected" \| "range" \| "outside" \| "disabled" \| "events" \| "view" \| "footer", string>>` |  | Extra classes for the inner parts. range goes on the cells that carry the band, the day keys on the day buttons. |
| `className?` | `string` |  | Extra classes for the root. Override the tone vars here, e.g. [--tone-soft:...] for a custom band. |

## Examples

- **Booking a stay**: Range mode with booked nights struck through, a 2 night minimum (minDays 3) and the total in the footer.
- **Several days**: multiple with maxSelected 3 and weekends disabled; at the limit the other days wait until one is removed.
- **Two months, paged**: numberOfMonths 2 with pagedNavigation: the arrows move both months. They stack below 640px; here a container query on classNames.months also stacks them in a narrow panel.
- **Prices under each day**: renderDay puts a nightly price under the number; size lg leaves room for two lines.
- **Locales**: Names, digits and the first day of the week come from Intl: Sunday in en-US, Monday in ro-RO and de-DE.
- **Birthday**: max is today; click the caption to jump by month, then by year. Today brings you back.
- **Sizes, shapes, week numbers**: 32px cells with rounded squares and ISO week numbers, next to 44px circles.
- **Custom color and events**: Any CSS color for the selection, and up to three dots a day from tones or colors.

## See also

- [Date Picker](https://wingo-ui.com/components/date-picker) ([Markdown](https://wingo-ui.com/components/date-picker.md))
- [Date Range Picker](https://wingo-ui.com/components/date-range-picker) ([Markdown](https://wingo-ui.com/components/date-range-picker.md))
- [Time Picker](https://wingo-ui.com/components/time-picker) ([Markdown](https://wingo-ui.com/components/time-picker.md))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))

---

Canonical: https://wingo-ui.com/components/calendar
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
