# Calendar View

> A React event calendar for appointments and meetings: month, week, day and agenda views, a now line and drag to reschedule.

- Type: React component (Data Display)
- 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-view`
- Docs: https://wingo-ui.com/components/calendar-view

## Interaction

Switch between Month, Week, Day and Agenda in the toolbar; the arrows move a period back or forward and Today jumps home, while the body slides in the direction you went and the title announces the new range. Click or tap an event to open its details (time, location, attendees) in a popover next to it; on a phone they rise as a bottom sheet. A month cell that has more events than fit shows "+2 more", which lists the whole day. With editable on and a mouse or pen, drag an event to another time or day (it snaps to the slot and springs into place while the original stays as a faint ghost) and drag its bottom edge to change the length; the change is applied at once and undone if onEventChange returns false. With onSlotSelect, click an empty slot to pick an hour or drag down the grid (or across days in the month) to select a range. Click a day number or a week column heading to open that day. Keyboard: the month is one Tab stop; arrows move by day and week, Home / End go to the start and end of the week, PageUp / PageDown change the month (Shift for a year), Enter opens the day's events, and Tab moves on to the events inside the day. In week, day and agenda PageUp / PageDown change the period while focus is inside, and events are buttons in time order read as the title and the time range. Escape closes the details and returns focus to the event. On a phone (or whenever the calendar is narrower than 576px) month and week turn into the agenda (or the compact month with dots and the day's list below, or the day view), agenda days have sticky headings and open at today, rows are 56px tall, and you swipe left or right on the day view and the compact month to change the period. Dragging events is off on touch so the page still scrolls. Reduced motion replaces the slides with fades.

## Installation

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

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-view` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/calendar-view.tsx`

npm dependencies: `npm i @dnd-kit/core date-fns lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Empty State](https://wingo-ui.com/components/empty-state) ([Markdown](https://wingo-ui.com/components/empty-state.md))
- [Popover](https://wingo-ui.com/components/popover) ([Markdown](https://wingo-ui.com/components/popover.md))
- [Progress](https://wingo-ui.com/components/progress) ([Markdown](https://wingo-ui.com/components/progress.md))
- [Segmented Control](https://wingo-ui.com/components/segmented-control) ([Markdown](https://wingo-ui.com/components/segmented-control.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))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.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 { Plus } from "lucide-react"
import { Button } from "@/components/ui/button"
import { CalendarView, type CalendarEvent } from "@/components/ui/calendar-view"

const appointments: CalendarEvent[] = [
  { id: "p1", title: "Consultation: Emma Carter", start: new Date(2026, 8, 24, 9, 0), end: new Date(2026, 8, 24, 10, 0), tone: "info", location: "Room 1" },
  { id: "p2", title: "Cleaning: Irene Vance", start: new Date(2026, 8, 24, 11, 30), end: new Date(2026, 8, 24, 12, 30), tone: "success" },
  { id: "p3", title: "Supplies delivery (UPS)", start: new Date(2026, 8, 24), end: new Date(2026, 8, 24), allDay: true },
]

export function Appointments() {
  const [events, setEvents] = useState(appointments)
  return (
    <CalendarView
      events={events}
      defaultDate={new Date(2026, 8, 24)}
      editable
      onEventChange={(event, change) =>
        setEvents((list) => list.map((e) => (e.id === event.id ? { ...e, ...change } : e)))
      }
      toolbarActions={<Button size="sm" leftIcon={<Plus />}>New appointment</Button>}
      labels={{
        more: (n) => `${n} more`,
        noEvents: "No appointments",
      }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `events?` | `CalendarEvent[]` | `"[]"` | The events: { id, title, start: Date, end: Date, allDay?, tone?, color?, location?, description?, attendees?: { name, avatar? }[], editable?, meta? }. An all-day event ending at local midnight ends the day before; an end at or before the start reads as 30 minutes (with a dev warning). Events crossing midnight show a block per day; multi-day events span the month cells as one bar per week. |
| `defaultView?` | `"month" \| "week" \| "day" \| "agenda"` | `"month"` | The view it opens in. Control it with view and onViewChange. |
| `view?` | `"month" \| "week" \| "day" \| "agenda"` |  | The current view (controlled); pair it with onViewChange. |
| `onViewChange?` | `(view: CalendarViewMode) => void` |  | Fires when the switch, a day number or a week column heading changes the view. |
| `views?` | `CalendarViewMode[]` | `"[\"month\", \"week\", \"day\", \"agenda\"]"` | The views in the switch, in this order. A single view hides the switch. |
| `defaultDate?` | `Date` |  | The focused date it opens on. Without it the server renders the first event's day and the browser moves to today after mount (no hydration mismatch). |
| `date?` | `Date` |  | The focused date (controlled); pair it with onDateChange. |
| `onDateChange?` | `(date: Date) => void` |  | Fires on prev / next / Today, on arrow keys in the month and when a day is picked. |
| `mobileView?` | `"agenda" \| "day" \| "month"` | `"agenda"` | What phones get instead of month and week (a container query: whenever the calendar is narrower than 576px, so a narrow side panel gets it too; both layouts are in the server HTML): the agenda of that month or week, the focused day, or a compact month with dots and the chosen day's events listed underneath. The phone switch only offers views that make sense there. |
| `layout?` | `"responsive" \| "desktop" \| "mobile"` | `"responsive"` | responsive switches to the phone layout when the calendar itself is narrower than 576px; desktop and mobile force one layout, e.g. a phone frame on a wide screen. |
| `variant?` | `"surface" \| "outline" \| "plain"` | `"surface"` | surface: a raised panel. outline: a hairline border on the page. plain: no container, for a page that is only the calendar. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The tone of events without their own tone or color: chips, blocks, agenda bars and dots. |
| `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). |
| `dayStartHour?` | `number` | `0` | The first hour of the week and day grids. |
| `dayEndHour?` | `number` | `24` | The hour the week and day grids end at (at least one hour after dayStartHour). |
| `scrollToHour?` | `number` | `8` | Where the week and day grids open. Moving to another week keeps the scroll position you left. |
| `slotMinutes?` | `15 \| 30 \| 60` | `"30"` | The snap for creating, moving and resizing events. Half-hour lines are dashed whenever the slot is under an hour. |
| `hourHeight?` | `number` | `48` | Pixels per hour in the week and day grids. Touch screens get at least 56px so short events stay tappable. |
| `height?` | `number` | `600` | Height of the scrolling week / day grid and the most the agenda grows to before it scrolls, in px. |
| `maxEventsPerDay?` | `number` | `3` | Chips per month cell before the last line turns into "+N more", which lists the day in a popover (a sheet on phones). |
| `showWeekNumbers?` | `boolean` | `false` | ISO week numbers in a narrow column at the start of each month row. |
| `showNowIndicator?` | `boolean` | `true` | A red line with a dot at the current time in today's column of the week and day grids; it moves once a minute. |
| `hideWeekends?` | `boolean` | `false` | The week view shows the five working days of the locale. |
| `editable?` | `boolean` | `false` | Drag events to move them and drag their bottom edge to resize, with a mouse or pen, snapped to slotMinutes. Events can opt out with editable: false. Off on touch screens, where dragging would fight scrolling. |
| `onEventChange?` | `(event: CalendarEvent, change: CalendarEventChange) => (false \| void)` |  | After a move or resize. The change shows at once; return false to put the event back. Update your events with the new times. |
| `onEventClick?` | `(event: CalendarEvent, domEvent: MouseEvent<HTMLElement>) => void` |  | Every click on an event. Call domEvent.preventDefault() to skip the built-in details (open your own drawer instead). |
| `showEventDetails?` | `boolean` | `true` | The built-in details on click: a popover next to the event, a bottom sheet on phones. |
| `renderEventDetails?` | `(event: CalendarEvent) => ReactNode` |  | Replaces the default details (title, time, location, description, attendees, meta) inside the popover or sheet. |
| `openEventId?` | `string \| null` |  | The event whose details are open (controlled), e.g. to open a new event right after onSlotSelect adds it. |
| `defaultOpenEventId?` | `string \| null` | `""` | The event whose details are open at first. In the playground try c26 (Implant follow-up on the 24th). |
| `onOpenEventIdChange?` | `(id: string \| null) => void` |  | Fires when details open or close. |
| `onSlotSelect?` | `(slot: { start: Date; end: Date; allDay: boolean }) => void` |  | Makes empty time selectable: click a slot for an hour, or drag down the grid (desktop) with a ghost "New event" showing the range; in the month click a day or drag across days for all-day ranges. Tap on touch. |
| `renderEvent?` | `(event: CalendarEvent, info: { view: CalendarViewMode }) => ReactNode` |  | Replaces the content of chips and blocks; the button, position, tone and drag stay. |
| `toolbar?` | `boolean` | `true` | The title, prev / Today / next and the view switch. |
| `toolbarActions?` | `ReactNode` |  | Buttons at the end of the toolbar, like "New appointment". |
| `loading?` | `boolean` | `false` | A thin indeterminate bar under the toolbar while events refresh; the current events stay. |
| `emptyAgenda?` | `ReactNode` |  | Shown when the agenda range has no events. Defaults to a small EmptyState with labels.noEvents. |
| `agendaDays?` | `number` | `14` | How many days the agenda view lists from the focused date; prev / next move by that many. |
| `locale?` | `string` | `""` | Month and weekday names, times (09:30 or 9:30 AM) and the first day of the week. Empty uses UIProvider, then the page language. |
| `labels?` | `Partial<CalendarViewLabels>` |  | Every string, English by default: today, previous, next, month, week, day, agenda, allDay, more(n), noEvents, newEvent, weekNumber, eventTime(start, end), eventCount(n), views, loading, drag { start, end, cancel }, details { location, attendees, close, title }. DEFAULT_CALENDAR_VIEW_LABELS has them all. |
| `classNames?` | `Partial<Record<"toolbar" \| "title" \| "nav" \| "switch" \| "actions" \| "body" \| "month" \| "weekdays" \| "day" \| "week" \| "event" \| "more" \| "weekHeader" \| "allDay" \| "scroll" \| "gutter" \| "grid" \| "now" \| "agenda" \| "agendaDay" \| "agendaEvent" \| "details", string>>` |  | Extra classes for the inner parts. |
| `children?` | `ReactNode` |  | Custom composition: the parts below in place of the built-in toolbar and body, sharing the calendar's state. |

## Parts

### CalendarViewToolbar (component)

The toolbar: live-region title, prev / Today / next, the view switch (a phone switch below 576px) and actions. Props: { actions?: ReactNode } & ComponentProps<"div">.

### CalendarMonth (component)

The month grid (role="grid") with lanes for chips and multi-day bars, "+N more" and keyboard roving. Takes div props.

### CalendarMonthCompact (component)

The phone month: day buttons with up to three tone dots and the chosen day's events underneath. Takes div props.

### CalendarWeek (component)

The time grid: day headings, the all-day row, the hour gutter, overlapping blocks, the now line and the selection ghost. Props: { mode?: "week" | "day" } & ComponentProps<"div">.

### CalendarAgenda (component)

Days with events as sticky headings over 56px rows (time, tone bar, title, location); scrolls to today on mount. Props: { range?: { start: Date; end: Date } } & ComponentProps<"div">.

### CalendarEventChip (component)

The month chip on its own: a tinted button with a 3px start bar in the event's tone. Props: { event: CalendarEvent; time?: ReactNode; tone?: Tone } & ComponentProps<"button">.

### layoutDayEvents (function)

The overlap layout of one day: segments with start / end minutes and column, columns and span (greedy columns, each event stretching over the free columns to its right). Signature: (events: CalendarEvent[], day: Date) => CalendarDayLayout[].

### getVisibleRange (function)

The first and last visible day of a view, both inclusive local midnights. Use it to fetch events for the screen. Signature: (view, date, weekStartsOn?, agendaDays?) => { start: Date; end: Date }.

### DEFAULT_CALENDAR_VIEW_LABELS (constant)

The English strings, to spread under your own or translate once in UIProvider defaults. Type: CalendarViewLabels.

### CalendarEvent (type)

One event: id, title, start and end (Dates; an all-day end at midnight is exclusive), allDay, tone or color, location, description, attendees ({ name, avatar }), editable (false keeps it still) and meta for the details.

### CalendarEventChange (type)

What onEventChange receives after a move or resize: the new start, end and allDay.

### CalendarSlotSelection (type)

What onSlotSelect receives: the selected start, end and allDay.

### CalendarViewMode (type)

"month" | "week" | "day" | "agenda".

## Examples

- **Sales week**: Week and day only, weekends hidden, 15-minute snapping, drag to reschedule.
- **Agenda only**: One view hides the switch: a phone-sized list of scheduled deliveries with sticky days.
- **Busy month**: Three chips per day then "+N more", a multi-day bar and ISO week numbers.
- **Select a range to create**: onSlotSelect adds the event and openEventId opens its details right away.
- **Day view with overlaps**: Three parallel appointments share the column; the lunch break is locked with editable: false.
- **Phone: compact month**: layout="mobile" with mobileView="month": dots per day, the day's list below, swipe for the next month.

## See also

- [Calendar](https://wingo-ui.com/components/calendar) ([Markdown](https://wingo-ui.com/components/calendar.md))
- [Scheduler](https://wingo-ui.com/components/scheduler) ([Markdown](https://wingo-ui.com/components/scheduler.md))
- [Date Picker](https://wingo-ui.com/components/date-picker) ([Markdown](https://wingo-ui.com/components/date-picker.md))
- [Timeline](https://wingo-ui.com/components/timeline) ([Markdown](https://wingo-ui.com/components/timeline.md))
- [Popover](https://wingo-ui.com/components/popover) ([Markdown](https://wingo-ui.com/components/popover.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))

---

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