# Calendar

> The full calendar: a toolbar with the view switcher, today and previous/next, over day, week, month and agenda views, with keyboard shortcuts.

A shadcn/ui block from the [@calendarcn](https://calendarcn.xyz/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/calendarcn/calendar).

- Page, with a live preview: https://shadcn.exchange/r/calendarcn/calendar
- Screenshot: https://shadcn.exchange/shot/calendarcn/calendar/light.png
- Registry item: https://calendarcn.xyz/r/calendar.json
- Listed: 2026-11-05, source last changed 2026-11-05

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/calendarcn/calendar.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `date-fns`, `lucide-react`
- Registry items: `@calendarcn/time-grid`, `@calendarcn/month-view`, `@calendarcn/agenda-view`, `button`, `dropdown-menu`, `kbd`

## Files

### `src/components/calendar.tsx`

```tsx
"use client";

import { addDays, format } from "date-fns";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";

import { cn } from "@/lib/utils";
import { useCalendarShortcuts } from "@/hooks/use-calendar-shortcuts";
import { Button } from "@/components/ui/button";
import { ViewDropdown } from "@/components/view-dropdown";
import { AGENDA_DAYS } from "@/lib/event-utils";

import { AgendaView } from "./agenda-view";
import {
  CalendarProvider,
  useCalendar,
  useCalendarConfig,
} from "./calendar-provider";
import { MonthView } from "./month-view";
import { TimeGrid } from "./time-grid";
import type {
  CalendarEvent,
  CalendarProviderProps,
  ViewType,
} from "./calendar-types";
import type { CalendarProps } from "./week-view-types";

/** Hour the time grid opens on unless `scrollToHour` says otherwise */
const DEFAULT_SCROLL_HOUR = 8;

/** The period previous and next move by in each view, for their labels */
const PERIOD_LABELS: Record<ViewType, string> = {
  day: "day",
  week: "week",
  month: "month",
  agenda: `${AGENDA_DAYS} days`,
};

/**
 * The full calendar: a toolbar with the title, view switcher, today and
 * previous/next, over a day, week, month or agenda view. Takes every
 * `CalendarProvider` prop, so events, view and date can be controlled or
 * left to the calendar.
 */
export function Calendar<TEvent extends CalendarEvent = CalendarEvent>(
  props: CalendarProps<TEvent>,
) {
  const {
    scrollToHour = DEFAULT_SCROLL_HOUR,
    shortcuts = true,
    className,
    children,
  } = props;

  // The provider reads only its own props, so the calendar's extra ones pass
  // through harmlessly. Callers are type-checked against `CalendarProps<TEvent>`
  // (which keeps `createEvent` required for custom event types); inside, the
  // event type is erased, as CalendarProvider does itself, because TypeScript
  // can't check JSX props against the generic conditional type.
  const providerProps: CalendarProviderProps = {
    ...(props as CalendarProviderProps),
    children: (
      <>
        <CalendarShortcuts enabled={shortcuts} />
        <div className={cn("flex h-full min-h-0 flex-col", className)}>
          <CalendarToolbar />
          <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
            <CalendarBody scrollToHour={scrollToHour} />
          </div>
        </div>
        {children}
      </>
    ),
  };

  return <CalendarProvider {...providerProps} />;
}

interface CalendarShortcutsProps {
  enabled: boolean;
}

/** Turns on the single-key shortcuts. A component so the hook runs inside the provider */
function CalendarShortcuts({ enabled }: CalendarShortcutsProps) {
  useCalendarShortcuts({ enabled });
  return null;
}

/** Title, view switcher, today, and previous and next */
function CalendarToolbar() {
  const {
    view,
    currentDate,
    monthName,
    year,
    weekNumber,
    numberOfDays,
    viewSettings,
    goToToday,
    goToPrev,
    goToNext,
    switchView,
    setNumberOfDays,
    toggleWeekends,
    toggleDeclinedEvents,
    toggleWeekNumbers,
  } = useCalendar();
  const { locale } = useCalendarConfig();
  const periodLabel = PERIOD_LABELS[view];

  return (
    <header className="@container/header flex h-12 shrink-0 items-center gap-1 border-b px-2 @md/header:gap-2 @md/header:px-3">
      <h2 className="min-w-0 flex-1 truncate text-sm @xs/header:text-base">
        <span className="font-semibold">
          {monthName} {year}
        </span>
        <span className="ml-2 hidden text-xs text-muted-foreground @lg/header:inline">
          {view === "day" && format(currentDate, "EEEE, MMM d", { locale })}
          {view === "week" && `Week ${weekNumber}`}
          {view === "agenda" &&
            `${format(currentDate, "MMM d", { locale })} \u2013 ${format(addDays(currentDate, AGENDA_DAYS - 1), "MMM d", { locale })}`}
        </span>
      </h2>
      <div className="hidden @xs/header:block">
        <ViewDropdown
          view={view}
          numberOfDays={numberOfDays}
          viewSettings={viewSettings}
          onSwitchView={switchView}
          onSetNumberOfDays={setNumberOfDays}
          onToggleWeekends={toggleWeekends}
          onToggleDeclinedEvents={toggleDeclinedEvents}
          onToggleWeekNumbers={toggleWeekNumbers}
        />
      </div>
      <Button
        variant="secondary"
        size="sm"
        className="px-2 @md/header:px-3"
        onClick={goToToday}
      >
        Today
      </Button>
      <div className="flex items-center">
        <Button
          variant="ghost"
          size="icon"
          className="size-8 text-muted-foreground"
          onClick={goToPrev}
        >
          <ChevronLeftIcon className="size-4" />
          <span className="sr-only">Previous {periodLabel}</span>
        </Button>
        <Button
          variant="ghost"
          size="icon"
          className="size-8 text-muted-foreground"
          onClick={goToNext}
        >
          <ChevronRightIcon className="size-4" />
          <span className="sr-only">Next {periodLabel}</span>
        </Button>
      </div>
    </header>
  );
}

interface CalendarBodyProps {
  scrollToHour: number;
}

/** The view the provider is on */
function CalendarBody({ scrollToHour }: CalendarBodyProps) {
  const { view } = useCalendar();

  if (view === "month") {
    return <MonthView />;
  }

  if (view === "agenda") {
    return <AgendaView />;
  }

  // One grid for day and week: switching re-lays it out instead of
  // remounting, so the vertical scroll position is kept
  return <TimeGrid view={view} scrollToHour={scrollToHour} />;
}
```

### `src/components/view-dropdown.tsx`

```tsx
"use client";

import { CheckIcon, ChevronDownIcon } from "lucide-react";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Kbd, KbdGroup } from "@/components/ui/kbd";

import type { ViewSettings, ViewType } from "@/components/week-view-types";

/** Display labels for each view type */
const VIEW_LABELS: Record<ViewType, string> = {
  day: "Day",
  week: "Week",
  month: "Month",
  agenda: "Agenda",
};

/** View mode options with their shortcut labels */
const VIEW_OPTIONS: Array<{
  value: ViewType;
  label: string;
  shortcuts: Array<string>;
}> = [
  { value: "day", label: "Day", shortcuts: ["1", "or", "D"] },
  { value: "week", label: "Week", shortcuts: ["0", "or", "W"] },
  { value: "month", label: "Month", shortcuts: ["M"] },
  { value: "agenda", label: "Agenda", shortcuts: ["A"] },
];

/** Number-of-days options for the submenu */
const DAYS_OPTIONS = [2, 3, 4, 5, 6, 7, 8, 9] as const;

/**
 * Shortcut hints inside menu items drop the key-cap background. Classes
 * rather than a Kbd variant, so the file works with the stock shadcn/ui Kbd.
 */
const PLAIN_KBD_CLASS = "h-auto min-w-0 rounded-none bg-transparent px-0";

interface ViewDropdownProps {
  view: ViewType;
  numberOfDays: number;
  viewSettings: ViewSettings;
  onSwitchView: (view: ViewType) => void;
  onSetNumberOfDays: (count: number) => void;
  onToggleWeekends: () => void;
  onToggleDeclinedEvents: () => void;
  onToggleWeekNumbers: () => void;
}

export function ViewDropdown({
  view,
  numberOfDays,
  viewSettings,
  onSwitchView,
  onSetNumberOfDays,
  onToggleWeekends,
  onToggleDeclinedEvents,
  onToggleWeekNumbers,
}: ViewDropdownProps) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="secondary" size="sm" className="gap-1 px-3">
          {VIEW_LABELS[view]}
          <ChevronDownIcon className="size-4 text-muted-foreground" />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" sideOffset={8} className="w-56">
        {VIEW_OPTIONS.map((option) => (
          <DropdownMenuItem
            key={option.value}
            onClick={() => onSwitchView(option.value)}
          >
            <CheckIcon
              className={cn(
                "size-4 shrink-0",
                view !== option.value && "invisible",
              )}
            />
            {option.label}
            <KbdGroup className="ml-auto">
              {option.shortcuts.map((s) =>
                s === "or" ? (
                  <span key={s} className="text-muted-foreground text-xs">
                    or
                  </span>
                ) : (
                  <Kbd key={s} className={PLAIN_KBD_CLASS}>
                    {s}
                  </Kbd>
                ),
              )}
            </KbdGroup>
          </DropdownMenuItem>
        ))}

        <DropdownMenuSeparator />

        {/* Number of days submenu */}
        <DropdownMenuSub>
          <DropdownMenuSubTrigger className="pl-8">
            Number of days
          </DropdownMenuSubTrigger>
          <DropdownMenuSubContent>
            {DAYS_OPTIONS.map((n) => (
              <DropdownMenuItem key={n} onClick={() => onSetNumberOfDays(n)}>
                <CheckIcon
                  className={cn(
                    "size-4 shrink-0",
                    numberOfDays !== n && "invisible",
                  )}
                />
                {n} days
                <Kbd className={cn(PLAIN_KBD_CLASS, "ml-auto")}>{n}</Kbd>
              </DropdownMenuItem>
            ))}
          </DropdownMenuSubContent>
        </DropdownMenuSub>

        <DropdownMenuSeparator />

        {/* View settings submenu */}
        <DropdownMenuSub>
          <DropdownMenuSubTrigger className="pl-8">
            View settings
          </DropdownMenuSubTrigger>
          <DropdownMenuSubContent className="w-56">
            <DropdownMenuCheckboxItem
              checked={viewSettings.showWeekends}
              onCheckedChange={() => onToggleWeekends()}
            >
              Weekends
              <KbdGroup className="ml-auto">
                <Kbd className={PLAIN_KBD_CLASS}>shift</Kbd>
                <Kbd className={PLAIN_KBD_CLASS}>&#8984;</Kbd>
                <Kbd className={PLAIN_KBD_CLASS}>E</Kbd>
              </KbdGroup>
            </DropdownMenuCheckboxItem>

            <DropdownMenuCheckboxItem
              checked={viewSettings.showDeclinedEvents}
              onCheckedChange={() => onToggleDeclinedEvents()}
            >
              Declined eve...
              <KbdGroup className="ml-auto">
                <Kbd className={PLAIN_KBD_CLASS}>shift</Kbd>
                <Kbd className={PLAIN_KBD_CLASS}>&#8984;</Kbd>
                <Kbd className={PLAIN_KBD_CLASS}>D</Kbd>
              </KbdGroup>
            </DropdownMenuCheckboxItem>

            <DropdownMenuCheckboxItem
              checked={viewSettings.showWeekNumbers}
              onCheckedChange={() => onToggleWeekNumbers()}
            >
              Week numbers
            </DropdownMenuCheckboxItem>
          </DropdownMenuSubContent>
        </DropdownMenuSub>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
```
