# React Calendar Component with Events: Month, Week, Day

> A React calendar component with events: month, week, day and agenda views, drag to reschedule, saves with rollback, and an agenda list on phones.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 19, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 10 min
- Canonical: https://wingo-ui.com/blog/react-calendar-component-with-events

## TL;DR

A React calendar component with events needs month, week, day and agenda views, overlapping events laid out side by side, drag to move and resize with a way to undo a failed save, and a list instead of a grid on phones. The Wingo UI Calendar View does this from an events array and a few callbacks; the smaller Calendar is a date picker that shows event dots under days.

You need a React calendar component with events: a clinic's appointments, a team's meetings, a sales rep's week. A month grid with a few colored chips is easy to build. Then the real data arrives. Two appointments overlap at 9:30, a conference runs across four days, someone drags a visit to Friday and the save fails, and on a phone the week view is seven columns that are each 50px wide. This guide covers what an event calendar needs, the props that matter in the Wingo UI [Calendar View](https://wingo-ui.com/components/calendar-view), how to fetch and save events, and what to show on a phone.

## What should a React calendar component with events handle?

Six things:

1. **Month, week, day and agenda views**, with prev, Today and next, and a title that says which range you are looking at.
2. **Overlapping events side by side** in the week and day grids, and multi-day events as one bar across the month cells.
3. **Details on click**: time, location, attendees, plus your own status or links.
4. **Drag to move and resize**, snapped to a slot, with a way to undo a move the server refuses.
5. **Selecting empty time** to create an event.
6. **A phone layout** that is a list, because a time grid does not fit.

If your question is "which room, car or person is free at 10 AM", you want a resource timeline instead, with resources as rows. That is a different component, covered in the [React resource scheduler guide](https://wingo-ui.com/blog/react-resource-scheduler). An event calendar answers "what is on my schedule".

## Does shadcn/ui have a calendar with events?

No. The [shadcn Calendar](https://ui.shadcn.com/docs/components/calendar) is a date picker built on React DayPicker: single days, ranges, month and year dropdowns. It has no week or day time grid. People searching for a shadcn calendar with events usually want two different things, and both exist in Wingo UI:

- A **date picker that shows events as dots**, so you can see busy days before you pick one. That is the [Calendar](https://wingo-ui.com/components/calendar), whose `events` prop takes `{ date, tone?, color?, label? }` and draws up to three dots under a day, with the labels read out by screen readers.
- A **React event calendar** that lays events out on a time grid. That is the Calendar View, the subject of the rest of this post.

## How do React event calendars compare?

react-big-calendar, FullCalendar and Mantine Schedule are the free MIT options, and the Wingo UI Calendar View is the Pro option you install as source. These are the names that come up when you search for "react calendar month week day" or a react big calendar alternative. Versions and facts are from each project's docs, repository and npm, as of October 2026.

| Option | License and price | Views | Drag to reschedule | Setup notes |
|---|---|---|---|---|
| [react-big-calendar](https://github.com/jquense/react-big-calendar) 1.20.0 | MIT, free | month, week, work_week, day, agenda | `withDragAndDrop` addon with `onEventDrop` and `onEventResize` | You must pick a localizer (Moment, Globalize, date-fns or Day.js), import its CSS and give the container a height |
| [FullCalendar](https://fullcalendar.io/docs/react) 7.1.3 | Standard is MIT and free; [Premium](https://fullcalendar.io/pricing) starts at $480 and adds timeline and vertical resource views | day grid, time grid and list (`listDay` to `listYear`) through plugins | `editable` with the interaction plugin | Pass at least one view plugin in `plugins` |
| [Mantine Schedule](https://mantine.dev/schedule/schedule/) 9.7.3 | MIT, free | day, week, month, year, plus an agenda button through `withAgenda` | `withEventsDragAndDrop` with `onEventDrop` | Part of Mantine; `layout="responsive"` swaps the month for a mobile month view |
| [Wingo UI Calendar View](https://wingo-ui.com/components/calendar-view) | Pro, source you own | month, week, day, agenda | `editable` with `onEventChange`, mouse and pen | Tailwind tokens, light and dark, a phone layout below 576px |

FullCalendar is the better choice if you also need resource timelines from the same vendor and are fine paying for Premium. react-big-calendar is a solid free default if you are happy to restyle its CSS. Mantine Schedule makes sense when your app already runs on Mantine. The Calendar View fits a Tailwind and shadcn style codebase where you want to read and edit every line.

## What props does the Calendar View need?

One: `events`. Each event is `{ id, title, start: Date, end: Date }` plus optional `allDay`, `tone` or `color`, `location`, `description`, `attendees`, `editable` and `meta`.

```bash
npx wingo-ui@latest add calendar-view
```

```tsx
"use client";

import { useState } from "react";
import { CalendarView, type CalendarEvent } from "@/components/ui/calendar-view";

const appointments: CalendarEvent[] = [
  { id: "p1", title: "Consultation: Emma Carter", start: new Date(2026, 9, 21, 9, 0), end: new Date(2026, 9, 21, 10, 0), tone: "info", location: "Room 1" },
  { id: "p2", title: "Cleaning: Irene Vance", start: new Date(2026, 9, 21, 9, 30), end: new Date(2026, 9, 21, 10, 30), tone: "success", location: "Room 2" },
  { id: "p3", title: "Dental congress, New York", start: new Date(2026, 9, 22), end: new Date(2026, 9, 25), allDay: true },
];

export function Appointments() {
  const [events, setEvents] = useState(appointments);
  return (
    <CalendarView
      events={events}
      defaultDate={new Date(2026, 9, 21)}
      defaultView="week"
      dayStartHour={8}
      dayEndHour={19}
      editable
      onEventChange={(event, change) =>
        setEvents((list) => list.map((e) => (e.id === event.id ? { ...e, ...change } : e)))
      }
      locale="en-US"
    />
  );
}
```

The two morning appointments overlap, so the week grid puts them in two columns, and the congress shows as one bar across three month cells (an all-day end at local midnight is exclusive). The props you change most after that:

- `views` and `defaultView`: the switch shows month, week, day and agenda by default; a single view hides it.
- `dayStartHour`, `dayEndHour`, `scrollToHour` (8 by default) and `hourHeight` (48px, at least 56px on touch screens).
- `slotMinutes`: 15, 30 or 60, the snap for moving, resizing and selecting.
- `hideWeekends`, `showWeekNumbers`, `maxEventsPerDay` (3 lines per month cell; a busier day shows two chips and "+N more").
- `toolbarActions` for a "New appointment" button, `labels` for every string, `variant` for `surface`, `outline` or `plain`.

The demo below opens in the week grid. With a mouse or pen, drag an appointment to another hour, then pull its bottom edge to make it longer.

> Live demo (Calendar View): Drag an appointment to another time or day: it snaps to the 30 minute slot and the line under the calendar shows the new time. Drag the bottom edge of an event to resize it, or click an empty slot to add one. Try it at [Calendar View](https://wingo-ui.com/components/calendar-view) and install it with `npx wingo-ui@latest add calendar-view`.

## How do I load events for the visible range?

Control `view` and `date`, and fetch whenever either changes. `getVisibleRange(view, date, weekStartsOn, agendaDays)` returns the first and last visible day as local midnights, both inclusive. Two details matter. Its `weekStartsOn` defaults to Monday, while the calendar follows the locale (Sunday in en-US), so pass the same value. And the end is the last day at midnight, so query up to the next midnight or you lose that day's events.

```tsx
"use client";

import { useEffect, useState } from "react";
import { addDays } from "date-fns";
import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { CalendarView, getVisibleRange, type CalendarEvent, type CalendarViewMode } from "@/components/ui/calendar-view";

type ApiEvent = Omit<CalendarEvent, "start" | "end"> & { start: string; end: string };
const toEvent = (e: ApiEvent): CalendarEvent => ({ ...e, start: new Date(e.start), end: new Date(e.end) });

export function ClinicCalendar() {
  const [view, setView] = useState<CalendarViewMode>("week");
  const [date, setDate] = useState(() => new Date());
  const [events, setEvents] = useState<CalendarEvent[]>([]);
  const [loading, setLoading] = useState(true);
  const [openId, setOpenId] = useState<string | null>(null);

  useEffect(() => {
    // 0 = Sunday, the same first day en-US gives the calendar
    const { start, end } = getVisibleRange(view, date, 0);
    const controller = new AbortController();
    setLoading(true);
    fetch(`/api/appointments?from=${start.toISOString()}&to=${addDays(end, 1).toISOString()}`, { signal: controller.signal })
      .then((response) => response.json() as Promise<ApiEvent[]>)
      .then((list) => {
        setEvents(list.map(toEvent));
        setLoading(false);
      })
      .catch((error) => {
        if (error.name !== "AbortError") setLoading(false);
      });
    return () => controller.abort();
  }, [view, date]);

  return (
    <CalendarView
      events={events}
      view={view}
      onViewChange={setView}
      date={date}
      onDateChange={setDate}
      loading={loading}
      locale="en-US"
      weekStartsOn={0}
      editable
      onEventChange={(event, change) => {
        // no appointments in the past
        if (change.start < new Date()) return false;
        const previous = events;
        setEvents((list) => list.map((e) => (e.id === event.id ? { ...e, ...change } : e)));
        fetch(`/api/appointments/${event.id}`, {
          method: "PATCH",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(change),
        })
          .then((response) => {
            if (!response.ok) setEvents(previous);
          })
          .catch(() => setEvents(previous));
      }}
      onSlotSelect={async (slot) => {
        const response = await fetch("/api/appointments", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ title: "New appointment", ...slot }),
        });
        if (!response.ok) return;
        const created = toEvent(await response.json());
        setEvents((list) => [...list, created]);
        setOpenId(created.id);
      }}
      openEventId={openId}
      onOpenEventIdChange={setOpenId}
      toolbarActions={<Button size="sm" leftIcon={<Plus />}>New appointment</Button>}
    />
  );
}
```

`loading` shows a thin bar under the toolbar while the current events stay on screen, so moving a week ahead does not flash an empty grid.

## How do I save a drag to reschedule?

The calendar shows the change the moment you drop, then calls `onEventChange(event, { start, end, allDay })`. Return `false` and the event goes straight back, which is how the example above refuses a move into the past. For a server save, update your events, send the request and restore the previous array if it fails. Passing a new `events` array clears the calendar's optimistic copy, so the restore is all you need. The snapshot is only safe for one save at a time; if moves can overlap, refetch the range after a failure instead.

`onSlotSelect` makes empty time selectable: click a slot for an hour, or drag down the grid for a range with a "New event" ghost. In the month, click a day or drag across days for an all-day range. Opening the new event through `openEventId` right after you add it shows its details at once, which is where your own Rename or Delete buttons can sit (see `meta` below).

Events can opt out with `editable: false`, for example a confirmed surgery that only the front desk may move.

## How does a React event calendar work on a phone?

It turns into a list. When the calendar is narrower than 576px, month and week switch to the `mobileView` you pick:

- `"agenda"` (the default): the days of that month or week as sticky headings over 56px rows with the time, a tone bar, the title and the location. It opens at today.
- `"day"`: the focused day as a time grid; swipe left or right to change the day.
- `"month"`: a compact month with up to three dots per day and the chosen day's events listed underneath, also with swipe.

The switch is a container query on the calendar itself, so a narrow side panel on a desktop gets the phone layout too, and both layouts are in the server HTML. `layout="mobile"` forces it, for example inside a phone frame. Below a 768px viewport, event details rise as a bottom sheet instead of a popover, for the reasons in the [React bottom sheet guide](https://wingo-ui.com/blog/react-bottom-sheet-for-the-web).

> Live demo (Calendar View): The phone layout with the compact month: tap a day with dots to list its appointments underneath, swipe sideways on a touch screen to change the month, and tap an appointment to open its details. Try it at [Calendar View](https://wingo-ui.com/components/calendar-view) and install it with `npx wingo-ui@latest add calendar-view`.

Dragging events is off on touch screens, because a drag that starts on an event would fight the page scroll. If people reschedule from their phones, give them a button in the details. Each event's `meta` renders at the end of the details popover or sheet:

```tsx
const withActions = (event: CalendarEvent): CalendarEvent => ({
  ...event,
  meta: (
    <Button size="sm" variant="soft" onClick={() => openRescheduleForm(event.id)}>
      Reschedule
    </Button>
  ),
});
```

`openRescheduleForm` is your own form, for instance a [Drawer](https://wingo-ui.com/components/drawer) with a date and time field. The same button also covers keyboard users, as the next section explains.

## How do I make an event calendar accessible?

Give every action a path that is not a drag. The month grid is one Tab stop: arrows move by day and week, Home and End go to the ends of the week, PageUp and PageDown change the month (with Shift, the year), Enter opens the day's events, and Tab moves on to the events inside the day. In week, day and agenda, events are buttons in time order, read as the title and the time range. Escape closes the details and returns focus to the event. The toolbar title is a live region, so a screen reader hears the new range after prev or next.

Moving an event by keyboard is not built into the grid (Enter opens the details), which is one more reason for a Reschedule button in `meta`. WCAG 2.2 [2.5.7 Dragging Movements](https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html) asks for a single-pointer alternative to any drag, and that button is it. Reduced motion replaces the slides between periods with fades.

## What are the common pitfalls?

- **Strings instead of Dates.** JSON gives you ISO strings. Convert `start` and `end` with `new Date()` when you load, as `toEvent` does above.
- **All-day ends.** An all-day event that ends at local midnight ends the day before, so a three-day conference starting on the 22nd ends at midnight on the 25th. An end at or before the start reads as 30 minutes, with a warning in development.
- **Mismatched week starts.** Pass the same `weekStartsOn` to `getVisibleRange` and the calendar, or a Sunday in en-US goes unfetched.
- **Hydration.** Without `defaultDate` or `date`, the server renders the first event's day and the browser moves to today after mount, so the HTML matches. A controlled `date` that starts as `new Date()`, as in the fetch example, is computed on the server and again in the browser, so near midnight or across time zones the two can land on different days. Start it from a fixed date or set it to today in an effect.
- **Very busy days.** Every overlapping event gets its own column, so a day with dozens of parallel bookings splits into columns too narrow to read. Show that kind of data in a [Data Table](https://wingo-ui.com/components/data-table) with filters, or split it by resource.

## How do I add it to my project?

Run `npx wingo-ui@latest add calendar-view` (or `calendar` for the date picker). The CLI copies the source into `components/ui`, adds the parts it uses, such as the [Popover](https://wingo-ui.com/components/popover), Avatar and Button, and installs `@dnd-kit/core`, `date-fns`, `motion` and `lucide-react`. Both the Calendar View and the Calendar are Pro components ([pricing](https://wingo-ui.com/pricing)). For picking a range instead of showing events, see the [React date range picker guide](https://wingo-ui.com/blog/react-date-range-picker). More single-component deep dives are in the [component guides category](https://wingo-ui.com/blog/category/components), and the [React form components guide](https://wingo-ui.com/blog/react-form-components-guide) covers the fields you will put in the reschedule form.

## Components in this post

- [Calendar View](https://wingo-ui.com/components/calendar-view) (Pro): A React event calendar for appointments and meetings: month, week, day and agenda views, a now line and drag to reschedule. Install: `npx wingo-ui@latest add calendar-view`
- [Calendar](https://wingo-ui.com/components/calendar) (Pro): A React calendar to pick one day, several days or a range, with disabled days, event dots, several months and a quick month / year jump. Install: `npx wingo-ui@latest add calendar`

## FAQ

### What is the best React calendar component with events?

For a free npm package, react-big-calendar and the FullCalendar Standard edition are both MIT and ship month, week, day and agenda or list views. Mantine Schedule fits apps already on Mantine, and the Wingo UI Calendar View fits when you want the source in your repo and a phone layout built in.

### Does shadcn/ui have a calendar with events?

The shadcn Calendar is a date picker built on React DayPicker, with single and range selection but no week or day time grid. For appointments you need a separate event calendar that renders events on a grid.

### What is a good react-big-calendar alternative?

FullCalendar is the closest match in features, and its Standard edition is free under MIT. If you want a calendar you install as source with Tailwind tokens, a phone agenda and drag to reschedule, the Wingo UI Calendar View is a Pro alternative.

### How do I save a drag to reschedule in a React calendar?

Update your events at once, send the new start and end to the server, and put the old array back if the request fails. In the Calendar View, onEventChange receives the event and its new times, and returning false undoes the move on the spot.

### How should an event calendar look on a phone?

A seven column week grid does not fit 390px, so show an agenda list grouped by day, a single day, or a compact month with dots and the chosen day listed underneath. Event details should open as a bottom sheet instead of a popover.

---

Source: https://wingo-ui.com/blog/react-calendar-component-with-events
