Bloom Logo

Event Calendar

A month-view grid scheduler display mapping calendar events, color states, and custom action callbacks.

importimport { EventCalendar } from "@/components/ui/eventCalendar/eventCalendar";
$npx @bloomui-react/cli add eventCalendar
eventCalendar.tsx
"use client";

import { Icon } from "@iconify/react";
import * as React from "react";
import { Button } from "@/components/ui/button/button";
import { designRadius } from "@/lib/design-system";
import { cn } from "@/lib/utils";

export interface CalendarEvent {
  id: string;
  title: string;
  startDate: Date;
  endDate: Date;
  color?:
    | "primary"
    | "secondary"
    | "success"
    | "warning"
    | "danger"
    | "default";
}

export interface EventCalendarProps {
  className?: string;
  events?: CalendarEvent[];
  onSelectDate?: (date: Date) => void;
  onEventClick?: (event: CalendarEvent) => void;
  radius?: keyof typeof designRadius;
}

const colorMap = {
  default:
    "bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200 border-zinc-200 dark:border-zinc-700",
  primary:
    "bg-sky-50 text-sky-700 dark:bg-sky-950/40 dark:text-sky-400 border-sky-200 dark:border-sky-800",
  secondary:
    "bg-purple-50 text-purple-700 dark:bg-purple-950/40 dark:text-purple-400 border-purple-200 dark:border-purple-800",
  success:
    "bg-emerald-50 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-400 border-emerald-200 dark:border-emerald-800",
  warning:
    "bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-400 border-amber-200 dark:border-amber-800",
  danger:
    "bg-rose-50 text-rose-700 dark:bg-rose-950/40 dark:text-rose-400 border-rose-200 dark:border-rose-800",
};

export const EventCalendar: React.FC<EventCalendarProps> = ({
  className,
  events = [],
  onSelectDate,
  onEventClick,
  radius = "md",
}) => {
  const [currentDate, setCurrentDate] = React.useState(new Date());
  const [selectedDate, setSelectedDate] = React.useState(new Date());

  const year = currentDate.getFullYear();
  const month = currentDate.getMonth();

  const monthNames = [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
  ];

  const firstDayOfMonth = new Date(year, month, 1).getDay();
  const daysInMonth = new Date(year, month + 1, 0).getDate();

  const days = React.useMemo(() => {
    const arr = [];
    for (let i = 0; i < firstDayOfMonth; i++) {
      arr.push(null);
    }
    for (let i = 1; i <= daysInMonth; i++) {
      arr.push(new Date(year, month, i));
    }
    return arr;
  }, [year, month, firstDayOfMonth, daysInMonth]);

  const handlePrevMonth = () => {
    setCurrentDate(new Date(year, month - 1, 1));
  };

  const handleNextMonth = () => {
    setCurrentDate(new Date(year, month + 1, 1));
  };

  const isToday = (date: Date) => {
    const today = new Date();
    return (
      date.getDate() === today.getDate() &&
      date.getMonth() === today.getMonth() &&
      date.getFullYear() === today.getFullYear()
    );
  };

  const isSelected = (date: Date) => {
    return (
      date.getDate() === selectedDate.getDate() &&
      date.getMonth() === selectedDate.getMonth() &&
      date.getFullYear() === selectedDate.getFullYear()
    );
  };

  const getEventsForDate = (date: Date) => {
    const d = new Date(
      date.getFullYear(),
      date.getMonth(),
      date.getDate(),
    ).getTime();
    return events.filter((event) => {
      const start = new Date(
        event.startDate.getFullYear(),
        event.startDate.getMonth(),
        event.startDate.getDate(),
      ).getTime();
      const end = new Date(
        event.endDate.getFullYear(),
        event.endDate.getMonth(),
        event.endDate.getDate(),
      ).getTime();
      return d >= start && d <= end;
    });
  };

  return (
    <div
      className={cn(
        "w-full border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 overflow-hidden shadow-xs",
        designRadius[radius],
        className,
      )}
    >
      <div className="flex items-center justify-between px-4 py-3 border-b border-zinc-200 dark:border-zinc-800">
        <h3 className="font-semibold text-zinc-900 dark:text-zinc-100">
          {monthNames[month]} {year}
        </h3>
        <div className="flex items-center gap-1">
          <Button
            variant="bordered"
            size="sm"
            onClick={handlePrevMonth}
            className="h-8 w-8 p-0"
          >
            <Icon icon="lucide:chevron-left" className="size-4" />
          </Button>
          <Button
            variant="bordered"
            size="sm"
            onClick={handleNextMonth}
            className="h-8 w-8 p-0"
          >
            <Icon icon="lucide:chevron-right" className="size-4" />
          </Button>
        </div>
      </div>

      <div className="grid grid-cols-7 border-b border-zinc-100 dark:border-zinc-800/50 bg-zinc-50 dark:bg-zinc-900/50 text-center text-xs font-semibold text-zinc-500 py-2">
        <div>Sun</div>
        <div>Mon</div>
        <div>Tue</div>
        <div>Wed</div>
        <div>Thu</div>
        <div>Fri</div>
        <div>Sat</div>
      </div>

      <div className="grid grid-cols-7 divide-x divide-y divide-zinc-100 dark:divide-zinc-800/40 bg-zinc-50/20 dark:bg-zinc-900/10">
        {days.map((date, idx) => (
          <div
            key={idx}
            onClick={() => {
              if (date) {
                setSelectedDate(date);
                onSelectDate?.(date);
              }
            }}
            className={cn(
              "min-h-[90px] p-1.5 flex flex-col justify-between transition-colors",
              date
                ? "cursor-pointer hover:bg-zinc-100/50 dark:hover:bg-zinc-800/30"
                : "bg-zinc-50/50 dark:bg-zinc-950/20 pointer-events-none",
              date && isSelected(date) && "bg-sky-500/5 dark:bg-sky-500/10",
            )}
          >
            {date ? (
              <div className="flex justify-between items-center">
                <span
                  className={cn(
                    "text-xs font-medium h-5 w-5 flex items-center justify-center rounded-full",
                    isToday(date)
                      ? "bg-sky-500 text-white font-semibold"
                      : "text-zinc-600 dark:text-zinc-400",
                  )}
                >
                  {date.getDate()}
                </span>
              </div>
            ) : (
              <div />
            )}

            <div className="mt-1 space-y-1 overflow-hidden">
              {date &&
                getEventsForDate(date).map((event) => (
                  <div
                    key={event.id}
                    onClick={(e) => {
                      e.stopPropagation();
                      onEventClick?.(event);
                    }}
                    className={cn(
                      "text-[10px] px-1.5 py-0.5 rounded-sm border truncate font-medium",
                      colorMap[event.color || "default"],
                    )}
                  >
                    {event.title}
                  </div>
                ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

EventCalendar.displayName = "EventCalendar";

Default

Standard monthly event calendar mapping current scheduled milestones.

events: CalendarEvent[]onSelectDate: (date: Date) => voidonEventClick: (event: CalendarEvent) => void

August 2026

Sun
Mon
Tue
Wed
Thu
Fri
Sat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Design Align
17
18
19
Sprint Planning
20
Multi-day Project Hackathon
21
Multi-day Project Hackathon
22
Multi-day Project Hackathon
23
24
25
26
27
28
29
30
31

Interactive Dialog Integration

Use a Dialog to schedule new events dynamically, including multi-day events.

events: CalendarEvent[]
Add custom events to the scheduler:

August 2026

Sun
Mon
Tue
Wed
Thu
Fri
Sat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Design Align
17
18
19
Sprint Planning
20
Multi-day Project Hackathon
21
Multi-day Project Hackathon
22
Multi-day Project Hackathon
23
24
25
26
27
28
29
30
31
API Reference

Props — EventCalendar

Properties for configuring the EventCalendar component.

PropTypeDefaultDescription
eventsCalendarEvent[][]List of event objects with dates, labels, and state colors to plot on the grid.
onSelectDate(date: Date) => voidundefinedCallback fired when a grid cell date is selected.
onEventClick(event: CalendarEvent) => voidundefinedCallback fired when an individual event block inside a day cell is clicked.
radiuskeyof typeof designRadius'md'Corner radius design token of the calendar card.