Free React resource timeline (a FullCalendar Premium alternative)
A resource timeline has people, rooms or machines down the side and time across the top. In several popular calendar libraries it is a paid tier (see the comparison for what each one includes). This tutorial builds one with Resource Scheduler, which is MIT and has no paid tier, in four steps. It is the same component running below.
Drag across empty days to create a booking, drag one to another row or day, grab an edge to resize it. Drop one on top of another: it turns red and is refused.
1. Install
Section titled “1. Install”npm install resource-schedulerWorks with React 18 and 19. Import the stylesheet once, anywhere in your app (it is scoped, so it does not restyle the rest of your page):
import "resource-scheduler/dist/resource-scheduler.css";2. Keep your bookings as one flat list
Section titled “2. Keep your bookings as one flat list”Most apps already have a table of bookings. Pass it as events and name the row on each one with resourceId; resources then only needs an id and a name. The component keeps no copy of your data: you hold the state and it tells you what the user did.
import { useState } from "react";import { ResourceScheduler, ViewType } from "resource-scheduler";import type { Resource, SchedulerEvent } from "resource-scheduler";
type Booking = SchedulerEvent & { resourceId: string };
const people: Resource[] = [ { id: "ann", name: "Ann Lee", events: [] }, { id: "bob", name: "Bob Ruiz", events: [] },];
export function Timeline() { const [events, setEvents] = useState<Booking[]>(initialBookings);
return ( <div style={{ height: 480 }}> <ResourceScheduler resources={people} events={events} initialView={ViewType.Week} /> </div> );}The component fills its parent, so the parent needs a height. An event is { id, title, startDate, endDate, color? }; endDate is exclusive, so a meeting from 9:00 to 10:00 does not overlap one that starts at 10:00.
3. Turn on drag, resize and create
Section titled “3. Turn on drag, resize and create”Each gesture is a callback, and a callback is also the opt-in: without onEventDrop a dropped event snaps back, and without onEventResize there are no resize handles. Each one is a plain state update.
<ResourceScheduler resources={people} events={events} initialView={ViewType.Week} onEventCreate={(event, resourceId) => setEvents((prev) => [...prev, { ...event, id: crypto.randomUUID(), resourceId }]) } onEventDrop={(event, _fromId, toId, start, end) => setEvents((prev) => prev.map((e) => (e.id === event.id ? { ...e, resourceId: toId, startDate: start, endDate: end } : e)) ) } onEventResize={(event, _resourceId, start, end) => setEvents((prev) => prev.map((e) => (e.id === event.id ? { ...e, startDate: start, endDate: end } : e))) }/>To open your own form instead of creating an event straight away, pass onSlotSelect; see Creating events.
4. Refuse conflicts
Section titled “4. Refuse conflicts”One prop stops double booking for drag, resize and create alike, and shows the refused place in red before the drop:
<ResourceScheduler /* ... */ eventOverlap={false} />For anything else (capacity, opening hours, a holiday), pass isValidDrop(event, { resourceId, start, end }). The same check is exported as isPlacementAllowed, so you can run it on your server before saving. See Conflict control.
Coming from FullCalendar
Section titled “Coming from FullCalendar”The model is close, and so are the callbacks:
| FullCalendar | Resource Scheduler |
|---|---|
eventDrop |
onEventDrop |
eventResize |
onEventResize |
select |
onSlotSelect |
eventClick |
onEventClick |
eventOverlap |
eventOverlap (also accepts a function) |
eventAllow / selectAllow |
isValidDrop |
Where next
Section titled “Where next”- Pick a finished starting point: room booking, shift roster, equipment rental.
- Keyboard and accessibility and theming are built in.
- Prefer to own the source? Copy it with the shadcn CLI: see Installation.