- Accordion
- Agent Thinking
- Alert
- Alert Dialog
- Announcement
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Checkbox Card
- Code Block
- Collapsible
- Color Picker
- Combobox
- Command
- Composer
- Composer Loader
- Context Menu
- Data Grid
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- File Upload
- Hover Card
- Input
- Input Group
- Input Otp
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Period Switcher
- Popover
- Progress
- Questionnaire
- Radio Card
- Radio Group
- Resizable
- Reveal
- Scroll Area
- Search Field
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Social Button
- Sonner
- Spinner
- Status Dot
- Switch
- Switch Card
- Table
- Tabs
- Tag Group
- Textarea
- Theme Toggle
- Time Field
- Toggle
- Toggle Group
- Tooltip
- Trace
- Tree View
- Agent Creator 01
- Agent Limits Card 01
- Agent Progress 01
- AI Chat 01
- AI Image Generation 01
- AI Profile 01
- Alerts Card 01
- Auth Card 01
- Calendar 01
- Composer 01
- Composer Attachments 01
- Composer Panel 01
- Finance Dashboard 01
- Home Dashboard 01
- Marketing Dashboard 01
- Medical Profile 01
- Meeting Scheduler 01
- Multi Agent Chat 01
- Notification Center 01
- Patient Info Card 01
- Project Board 01
- Questionnaire 01
- Settings Modal 01
- Sidebar 01
- Stat Cards 01
- Task List 01
- Web Search 01
- Activity Rings Card 01
- Area Chart Card 01
- Bar List Card 01
- Combo Chart Card 01
- Contributions Card 01
- Earnings Chart Card 01
- Funnel Chart Card 01
- Heatmap Chart Card 01
- Line Chart Card 01
- Most Active Days Card 01
- Orders Chart Card 01
- Radar Chart Card 01
- Radial Chart Card 01
- Revenue Chart Card 01
- Sankey Chart Card 01
- Scatter Chart Card 01
- Sleep Score Card 01
- Stage Bars Card 01
- Steps Card 01
August 2026
August 2026
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 31 | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 1 | 2 | 3 | 4 | 5 |
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>
);
} On this page