# Event Calendar

> Month calendar with event chips, +N overflow, month navigation and a per-day agenda.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/event-calendar
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/event-calendar.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/event-calendar-demo.tsx"
"use client"

import { EventCalendar, type CalendarEvent } from "@/components/event-calendar"

const events: CalendarEvent[] = [
  { id: "1", date: "2026-11-03", title: "Design review", time: "10:00", color: "violet" },
  { id: "2", date: "2026-11-05", title: "Sprint planning", time: "09:30", color: "blue" },
  { id: "3", date: "2026-11-05", title: "1:1 with Maya", time: "14:00", color: "green" },
  { id: "4", date: "2026-11-05", title: "Dentist", time: "17:30", color: "rose" },
  { id: "5", date: "2026-11-05", title: "Gym", time: "19:00", color: "amber" },
  { id: "6", date: "2026-11-12", title: "Launch day", time: "All day", color: "rose" },
  { id: "7", date: "2026-11-18", title: "Team lunch", time: "12:30", color: "amber" },
  { id: "8", date: "2026-11-24", title: "Quarterly OKRs", time: "11:00", color: "blue" },
  { id: "9", date: "2026-12-02", title: "Retro", time: "15:00", color: "green" },
]

export default function EventCalendarDemo() {
  return <EventCalendar events={events} year={2026} month={10} today="2026-11-07" />
}
```

## Source

```tsx title="components/event-calendar.tsx"
"use client"

import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"

import { cn } from "@/lib/utils"

export interface CalendarEvent {
  id: string
  /** "YYYY-MM-DD" */
  date: string
  title: string
  /** Pre-formatted time label */
  time?: string
  color?: "blue" | "green" | "amber" | "rose" | "violet"
}

export interface EventCalendarProps extends Omit<React.ComponentProps<"div">, "onSelect"> {
  events: CalendarEvent[]
  /** The month shown first */
  year: number
  /** 0-11 */
  month: number
  /** "YYYY-MM-DD" highlighted as today */
  today?: string
  locale?: string
  onSelectDate?: (date: string) => void
}

const dot = {
  blue: "bg-sky-500",
  green: "bg-emerald-500",
  amber: "bg-amber-500",
  rose: "bg-rose-500",
  violet: "bg-violet-500",
} as const

const chip = {
  blue: "bg-sky-500/15 text-sky-700 dark:text-sky-300",
  green: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
  amber: "bg-amber-500/15 text-amber-700 dark:text-amber-300",
  rose: "bg-rose-500/15 text-rose-700 dark:text-rose-300",
  violet: "bg-violet-500/15 text-violet-700 dark:text-violet-300",
} as const

const key = (y: number, m: number, d: number) => `${y}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`

export function EventCalendar({ events, year, month, today, locale = "en-US", onSelectDate, className, ...props }: EventCalendarProps) {
  const [cursor, setCursor] = React.useState({ year, month })
  const [selected, setSelected] = React.useState<string | null>(null)

  const first = new Date(cursor.year, cursor.month, 1)
  const daysInMonth = new Date(cursor.year, cursor.month + 1, 0).getDate()
  const lead = first.getDay()
  const cells = Array.from({ length: Math.ceil((lead + daysInMonth) / 7) * 7 }, (_, i) => {
    const day = i - lead + 1
    return day >= 1 && day <= daysInMonth ? day : null
  })

  const byDate = new Map<string, CalendarEvent[]>()
  for (const event of events) byDate.set(event.date, [...(byDate.get(event.date) ?? []), event])

  const weekdays = Array.from({ length: 7 }, (_, i) => new Date(2024, 0, 7 + i).toLocaleDateString(locale, { weekday: "short" }))
  const title = first.toLocaleDateString(locale, { month: "long", year: "numeric" })
  const move = (delta: number) => {
    const d = new Date(cursor.year, cursor.month + delta, 1)
    setCursor({ year: d.getFullYear(), month: d.getMonth() })
    setSelected(null)
  }
  const agenda = selected ? (byDate.get(selected) ?? []) : []

  return (
    <div {...props} className={cn("w-full max-w-3xl rounded-2xl border bg-card p-4", className)}>
      <div className="mb-3 flex items-center justify-between">
        <h3 className="text-base font-semibold" aria-live="polite">
          {title}
        </h3>
        <div className="flex gap-1">
          {[-1, 1].map((delta) => (
            <button
              key={delta}
              type="button"
              aria-label={delta < 0 ? "Previous month" : "Next month"}
              onClick={() => move(delta)}
              className="flex size-8 items-center justify-center rounded-md border transition-colors hover:bg-accent"
            >
              {delta < 0 ? <ChevronLeft className="size-4" /> : <ChevronRight className="size-4" />}
            </button>
          ))}
        </div>
      </div>

      <div className="grid grid-cols-7 border-l border-t text-xs">
        {weekdays.map((name) => (
          <div key={name} className="border-b border-r bg-muted/40 py-1.5 text-center font-medium text-muted-foreground">
            {name}
          </div>
        ))}
        {cells.map((day, i) => {
          if (day === null) return <div key={i} className="min-h-14 border-b border-r bg-muted/20 sm:min-h-24" />
          const id = key(cursor.year, cursor.month, day)
          const dayEvents = byDate.get(id) ?? []
          const isToday = id === today
          return (
            <button
              key={i}
              type="button"
              aria-label={`${[day, ...dayEvents.slice(0, 2).map((event) => event.title), ...(dayEvents.length > 2 ? [`+${dayEvents.length - 2} more`] : [])].join(" ")}, ${new Date(cursor.year, cursor.month, day).toLocaleDateString(locale, { month: "long", day: "numeric" })}, ${dayEvents.length} events`}
              aria-pressed={selected === id}
              onClick={() => {
                setSelected(id)
                onSelectDate?.(id)
              }}
              className={cn(
                "flex min-h-14 flex-col items-stretch gap-1 border-b border-r p-1 text-left outline-none transition-colors hover:bg-accent/60 focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring sm:min-h-24 sm:p-1.5",
                selected === id && "bg-accent",
              )}
            >
              <span
                className={cn(
                  "flex size-6 items-center justify-center self-start rounded-full text-xs tabular-nums",
                  isToday && "bg-primary font-semibold text-primary-foreground",
                )}
              >
                {day}
              </span>{" "}
              <span className="hidden flex-col gap-0.5 sm:flex">
                {dayEvents.slice(0, 2).map((event) => (
                  <span key={event.id} className={cn("truncate rounded px-1.5 py-0.5 text-[11px] leading-tight", chip[event.color ?? "blue"])}>
                    {event.title}{" "}
                  </span>
                ))}
                {dayEvents.length > 2 && <span className="px-1 text-[11px] text-muted-foreground">+{dayEvents.length - 2} more</span>}
              </span>
              <span className="flex gap-0.5 sm:hidden">
                {dayEvents.slice(0, 3).map((event) => (
                  <span key={event.id} className={cn("size-1.5 rounded-full", dot[event.color ?? "blue"])} />
                ))}
              </span>
            </button>
          )
        })}
      </div>

      {selected && (
        <div className="mt-4 rounded-xl border bg-background p-3" role="region" aria-label="Agenda">
          <h4 className="text-sm font-semibold">
            {new Date(`${selected}T00:00:00`).toLocaleDateString(locale, { weekday: "long", month: "long", day: "numeric" })}
          </h4>
          {agenda.length === 0 ? (
            <p className="mt-1 text-sm text-muted-foreground">Nothing scheduled.</p>
          ) : (
            <ul className="mt-2 grid gap-2">
              {agenda.map((event) => (
                <li key={event.id} className="flex items-center gap-2 text-sm">
                  <span className={cn("size-2 rounded-full", dot[event.color ?? "blue"])} />
                  <span className="font-medium">{event.title}</span>
                  {event.time && <span className="ml-auto text-xs tabular-nums text-muted-foreground">{event.time}</span>}
                </li>
              ))}
            </ul>
          )}
        </div>
      )}
    </div>
  )
}
```
