For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Calendar 01

A month view for events with an inbox and event details.

Installation

Set up the @cobalt registry first, as described in Installation, then add the block:

$ pnpm dlx shadcn@latest add @cobalt/calendar-01

Pass events with a unique id, a local date in YYYY-MM-DD format, and a title. The optional popupClassName scopes theme tokens to the popovers.

Source

"use client";

import { RiNotification3Line } from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";
import type { DayProps } from "react-day-picker";

import { CalendarEventDialog } from "@/components/blocks/calendar-01/components/calendar-event-dialog";
import { CalendarEventPopover } from "@/components/blocks/calendar-01/components/calendar-event-popover";
import {
  dateKey,
  useCalendar,
} from "@/components/blocks/calendar-01/components/calendar-state";
import type {
  CalendarController,
  CalendarEvent,
  CalendarSource,
} from "@/components/blocks/calendar-01/components/calendar-state";
import { CalendarToolbar } from "@/components/blocks/calendar-01/components/calendar-toolbar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Calendar as CalendarView } from "@/components/ui/calendar";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Empty, EmptyDescription } from "@/components/ui/empty";
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover";

export type {
  CalendarController,
  CalendarEvent,
  CalendarSource,
  CalendarTone,
} from "@/components/blocks/calendar-01/components/calendar-state";
export { useCalendar } from "@/components/blocks/calendar-01/components/calendar-state";
export { CalendarToolbar } from "@/components/blocks/calendar-01/components/calendar-toolbar";

const MAX_CHIPS = 4;
// Chip colours come from the calendar-chip tokens in the theme: a pale fill
// with a strong title in light, a deep tint with a bright title in dark. The
// fallbacks reuse the badge tones when the theme is not installed.
const CHIP_TONE: Record<string, string> = {
  lime: "bg-[var(--calendar-chip-lime-background,var(--badge-lime-background,var(--muted)))] text-[color:var(--calendar-chip-lime-foreground,var(--badge-lime-foreground,var(--foreground)))] group-hover/chip:bg-[color-mix(in_oklch,var(--calendar-chip-lime-background,var(--badge-lime-background,var(--muted))),var(--calendar-chip-lime-foreground,var(--badge-lime-foreground,var(--foreground)))_14%)]",
  rose: "bg-[var(--calendar-chip-rose-background,var(--badge-rose-background,var(--muted)))] text-[color:var(--calendar-chip-rose-foreground,var(--badge-rose-foreground,var(--foreground)))] group-hover/chip:bg-[color-mix(in_oklch,var(--calendar-chip-rose-background,var(--badge-rose-background,var(--muted))),var(--calendar-chip-rose-foreground,var(--badge-rose-foreground,var(--foreground)))_14%)]",
  purple:
    "bg-[var(--calendar-chip-purple-background,var(--badge-purple-background,var(--muted)))] text-[color:var(--calendar-chip-purple-foreground,var(--badge-purple-foreground,var(--foreground)))] group-hover/chip:bg-[color-mix(in_oklch,var(--calendar-chip-purple-background,var(--badge-purple-background,var(--muted))),var(--calendar-chip-purple-foreground,var(--badge-purple-foreground,var(--foreground)))_14%)]",
  blue: "bg-[var(--calendar-chip-blue-background,var(--badge-blue-background,var(--muted)))] text-[color:var(--calendar-chip-blue-foreground,var(--badge-blue-foreground,var(--foreground)))] group-hover/chip:bg-[color-mix(in_oklch,var(--calendar-chip-blue-background,var(--badge-blue-background,var(--muted))),var(--calendar-chip-blue-foreground,var(--badge-blue-foreground,var(--foreground)))_14%)]",
  cyan: "bg-[var(--calendar-chip-cyan-background,var(--badge-cyan-background,var(--muted)))] text-[color:var(--calendar-chip-cyan-foreground,var(--badge-cyan-foreground,var(--foreground)))] group-hover/chip:bg-[color-mix(in_oklch,var(--calendar-chip-cyan-background,var(--badge-cyan-background,var(--muted))),var(--calendar-chip-cyan-foreground,var(--badge-cyan-foreground,var(--foreground)))_14%)]",
};

// The Calendar primitive asks for one component per grid cell, so the events
// reach the cells through context: the month grid keeps a single component
// identity while events are added.
const DayContext = React.createContext<{
  calendar?: CalendarController;
  popupClassName?: string;
}>({});

function EventChip({
  event,
  date,
  calendar,
  popupClassName,
}: {
  event: CalendarEvent;
  date: Date;
  calendar: CalendarController;
  popupClassName?: string;
}) {
  return (
    <Popover
      open={calendar.openEventId === event.id}
      onOpenChange={(open) => calendar.setOpenEventId(open ? event.id : null)}
    >
      <PopoverTrigger
        render={
          <Button
            variant="unstyled"
            radius={6}
            className="group/chip w-full min-w-0"
          />
        }
        aria-label={`${event.title}${event.time ? `, ${event.time}` : ""}`}
      >
        <span
          className={cn(
            "flex h-[22px] w-full items-center justify-between gap-1 rounded-[6px] px-2 text-xs font-medium transition-colors duration-150 ease-out @max-lg:px-1 motion-reduce:transition-none",
            CHIP_TONE[event.tone ?? "blue"]
          )}
        >
          <span className="truncate">{event.title}</span>
          {event.time && (
            <span className="hidden shrink-0 font-normal opacity-70 tabular-nums @2xl:inline">
              {event.time}
            </span>
          )}
        </span>
      </PopoverTrigger>
      <CalendarEventPopover
        event={event}
        date={date}
        calendar={calendar}
        popupClassName={popupClassName}
      />
    </Popover>
  );
}

function MoreEvents({
  hidden,
  date,
  calendar,
  popupClassName,
}: {
  hidden: CalendarEvent[];
  date: Date;
  calendar?: CalendarController;
  popupClassName?: string;
}) {
  return (
    <Popover>
      <PopoverTrigger
        render={<Button variant="text" size="xs" className="self-start" />}
      >
        +{hidden.length}
        <span className="@max-lg:hidden"> more</span>
      </PopoverTrigger>
      <PopoverContent className={popupClassName} align="start">
        <PopoverTitle>More events</PopoverTitle>
        <PopoverDescription>
          {date.toLocaleDateString("en-US", {
            weekday: "long",
            month: "long",
            day: "numeric",
          })}
        </PopoverDescription>
        <ul className="flex flex-col gap-1.5">
          {hidden.map((event) => (
            <li key={event.id}>
              <Button
                variant="ghost"
                radius={8}
                onClick={() => calendar?.openEdit(event)}
                className="w-full justify-between gap-2"
              >
                <span className="truncate font-medium">{event.title}</span>
                <span className="shrink-0 text-muted-foreground tabular-nums">
                  {event.time ?? "All day"}
                </span>
              </Button>
            </li>
          ))}
        </ul>
      </PopoverContent>
    </Popover>
  );
}

function EventDay({ day, modifiers, children: _children, ...props }: DayProps) {
  const { calendar } = React.useContext(DayContext);
  const dayEvents = calendar?.eventsByDate.get(dateKey(day.date)) ?? [];
  const outside = Boolean(modifiers.outside);
  const selected = Boolean(modifiers.selected);
  // The latest events stay in the cell; the earlier ones go behind "more".
  const hidden = dayEvents.slice(0, Math.max(0, dayEvents.length - MAX_CHIPS));
  const shown = dayEvents.slice(hidden.length);

  return (
    <td {...props}>
      <div
        className={cn(
          "flex h-full min-h-[112px] min-w-0 flex-col p-2 text-sm @lg:min-h-[128px] @lg:rounded-[12px] @lg:shadow-xs",
          "@max-lg:rounded-none @max-lg:p-1",
          "bg-background dark:bg-muted",
          outside && "bg-border text-muted-foreground dark:bg-muted",
          selected && "ring-2 ring-primary ring-inset"
        )}
      >
        <span className="leading-4 font-medium">{day.date.getDate()}</span>
        <DayContext.Consumer>
          {({ popupClassName }) => (
            <>
              {hidden.length > 0 && (
                <MoreEvents
                  hidden={hidden}
                  date={day.date}
                  calendar={calendar}
                  popupClassName={popupClassName}
                />
              )}
              <div className="mt-auto flex min-w-0 flex-col gap-0.75">
                {calendar &&
                  shown.map((event) => (
                    <EventChip
                      key={event.id}
                      event={event}
                      date={day.date}
                      calendar={calendar}
                      popupClassName={popupClassName}
                    />
                  ))}
              </div>
            </>
          )}
        </DayContext.Consumer>
      </div>
    </td>
  );
}

/**
 * The month grid and the event dialog. It is built on the `Calendar`
 * primitive, so the week rows, outside days and weekday names come from it.
 * Drive it with `useCalendar`.
 */
export function CalendarGrid({
  calendar,
  popupClassName,
  className,
}: {
  calendar: CalendarController;
  popupClassName?: string;
  className?: string;
}) {
  const context = { calendar, popupClassName };
  return (
    <div className={cn("@container", className)}>
      <div className="calendar-01-weeks overflow-hidden rounded-[20px] bg-muted p-2.5 dark:bg-card @max-lg:rounded-none @max-lg:bg-border dark:@max-lg:bg-border @max-lg:p-0">
        <DayContext.Provider value={context}>
          <CalendarView
            // A new month remounts the grid so the week rows replay the
            // staggered entrance instead of swapping instantly.
            key={calendar.monthLabel}
            month={calendar.month}
            onMonthChange={calendar.setMonth}
            mode="single"
            selected={calendar.selectedDay}
            fixedWeeks
            showOutsideDays
            hideNavigation
            formatters={{
              formatWeekdayName: (day) =>
                day.toLocaleDateString("en-US", { weekday: "short" }),
            }}
            classNames={{
              root: "w-full",
              months: "relative flex w-full flex-wrap justify-center",
              month: "relative flex w-full shrink-0 flex-col p-0",
              month_caption: "sr-only",
              nav: "hidden",
              month_grid:
                "-m-2 w-[calc(100%+16px)] table-fixed border-separate [border-spacing:8px] @max-lg:m-0 @max-lg:w-full @max-lg:[border-spacing:1px]",
              weekdays: "",
              day: "h-px p-0 text-left align-top",
              weeks: "",
            }}
            components={{ Day: EventDay }}
          />
        </DayContext.Provider>
      </div>
      <CalendarEventDialog
        calendar={calendar}
        popupClassName={popupClassName}
      />
    </div>
  );
}

/**
 * A month of events with the calendars popover, month navigation, a day
 * picker and a create/edit dialog, in its own card. To put the controls in a
 * page header instead, use `useCalendar`, `CalendarToolbar` and
 * `CalendarGrid` directly.
 */
export function Calendar({
  events: initialEvents,
  sources,
  initialMonth,
  popupClassName,
  className,
}: {
  events: CalendarEvent[];
  sources?: CalendarSource[];
  initialMonth?: Date;
  popupClassName?: string;
  className?: string;
}) {
  const calendar = useCalendar({ events: initialEvents, initialMonth });
  const { monthEvents, monthLabel } = calendar;
  const notifications = monthEvents.slice(0, 3);

  return (
    <Card
      variant="outline"
      radius={20}
      role="region"
      aria-label="Event calendar"
      className={className}
    >
      <CardHeader className="flex flex-wrap items-center gap-2">
        <h2 className="mr-auto text-lg font-semibold">{monthLabel}</h2>
        <Popover>
          <PopoverTrigger
            render={
              <Button
                variant="outline"
                size="icon"
                tooltip="Notifications"
                aria-label={`Notifications, ${notifications.length} events`}
                className="relative"
              />
            }
          >
            <RiNotification3Line />
            {notifications.length > 0 && (
              <Badge
                className="pointer-events-none absolute -top-2 -right-2"
                variant="counter"
                tone="destructive"
                shape="circle"
              >
                {notifications.length}
              </Badge>
            )}
          </PopoverTrigger>
          <PopoverContent className={popupClassName}>
            <PopoverTitle>Notifications</PopoverTitle>
            <PopoverDescription>Events in {monthLabel}</PopoverDescription>
            {notifications.length ? (
              <ul className="flex flex-col gap-2">
                {notifications.map((event) => (
                  <li key={event.id} className="flex flex-col">
                    <span className="font-medium">{event.title}</span>
                    <span className="text-xs text-muted-foreground">
                      {new Date(`${event.date}T00:00:00`).toLocaleDateString(
                        "en-US",
                        { month: "short", day: "numeric" }
                      )}
                      {event.time ? ` · ${event.time}` : ""}
                    </span>
                  </li>
                ))}
              </ul>
            ) : (
              <Empty variant="ghost">
                <EmptyDescription>No events this month.</EmptyDescription>
              </Empty>
            )}
          </PopoverContent>
        </Popover>
        <CalendarToolbar
          calendar={calendar}
          sources={sources}
          popupClassName={popupClassName}
        />
      </CardHeader>
      <CardContent className="gap-0">
        <CalendarGrid calendar={calendar} popupClassName={popupClassName} />
      </CardContent>
    </Card>
  );
}