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 (the comparison lists what each one includes and costs). This post builds one with Resource Scheduler, which is MIT and has no paid tier, in four steps. The original tutorial has the same component running live.
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
npm install resource-scheduler
Works 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
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
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
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
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 |
Know the gaps. This is a younger library. RRULE strings, calendar sync with Google or Outlook, and Gantt dependencies are not built in. Read Alternatives before you switch.
Where next
- Finished starting points: room booking, shift roster, equipment rental.
- A full-stack version with Next.js and Prisma: Room booking app with Next.js and Prisma.
- Source, issues and the shadcn install: github.com/mdobaid311/resource-scheduler.
Top comments (0)