Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelog
  1. Home
  2. Blog
  3. Component guides
  4. React Resource Scheduler: Drag to Book Rooms and Cars
Auf Deutsch lesen
  1. Blog
  2. Component guides
  3. React Resource Scheduler: Drag to Book Rooms and Cars
Component guides
Component guides

React Resource Scheduler: Drag to Book Rooms and Cars

SR
Serban Rusu · Founder of Wingo UI
Oct 16, 2026 · 10 min read

On this page

0%
  1. What is a resource scheduler, and how is it different from a calendar?
  2. Which React resource scheduler options exist in 2026?
  3. What does a resource timeline look like in practice?
  4. How do you set up resources and bookings?
  5. How does drag to create, move and resize work?
  6. How do you stop double bookings?
  7. How should a React booking calendar work on a phone?
  8. How do you make a resource scheduler keyboard accessible?
  9. What breaks when a resource scheduler goes to production?
  10. When is a regular event calendar the better choice?
  11. How do you add the scheduler to a Next.js app?
  12. FAQ
    1. What is a resource scheduler in React?
    2. Is FullCalendar's resource timeline free?
    3. How do I prevent double bookings in a React scheduler?
    4. How should a booking calendar work on mobile?
    5. Should I use a resource scheduler or a regular calendar?

TL;DR

A React resource scheduler puts the things you book (rooms, cars, people) in rows and time in columns, so a free slot is a visible gap you drag across to book, and a booking is a bar you drag to move or stretch. It must refuse overlaps and unavailable time during the drag, re-check every booking on the server, and become an agenda with a Book button on a phone, where a 12 hour grid does not fit. The Wingo UI Scheduler does this in one component fed by resources, events and a few callbacks.

Published Oct 16, 2026

A React resource scheduler is the screen behind room booking, fleet planning and shift rotas: resources down the side, hours across the top, bookings as bars you can drag. It looks simple until people drag a meeting onto a taken room, book a car during its service, or both grab the Hudson room at 9 AM, and on a phone the 12 hour grid becomes a strip nobody can tap. This guide covers what a resource timeline needs, the props that matter in the Wingo UI Scheduler, how to keep bookings honest on the server, and what to show on a phone.

What is a resource scheduler, and how is it different from a calendar?

A calendar answers "what is on my day". A resource scheduler answers "which room, car or person is free at 2 PM". That changes the layout:

  • Rows are resources. Each meeting room, vehicle or team member gets a lane. Lanes can be grouped (Rooms, Team, Cars) and folded.
  • Columns are time slots. A day in 15, 30 or 60 minute slots, or a week of days.
  • Availability is part of the row. Off hours are shaded, maintenance or time off refuses bookings.
  • Bookings belong to one resource. Dragging to another row reassigns it.

Every room booking UI needs these four. If your users mostly look at their own appointments, a month and week calendar such as Calendar View fits better; for availability across many things you want a react calendar resource timeline.

Which React resource scheduler options exist in 2026?

The main choices are FullCalendar Premium, Bryntum Scheduler, Syncfusion, the open source react-big-schedule and the Wingo UI Scheduler. Here is what their own pages say as of October 2026:

OptionResource timelinePrice and licenseSource
FullCalendarTimeline View and Vertical Resource View are premiumStandard free, Premium from $480 (1 to 10 developers)pricing (opens in a new tab)
Bryntum Scheduleryes, it is the productfrom $680 per developer (price shown for 3 or more licenses)store (opens in a new tab)
Syncfusion React Schedulertimeline and resource viewscommercial; free community license for under $1M annual revenue, 5 or fewer developers, 10 or fewer employees and no more than $3M in outside capitalcommunity license (opens in a new tab)
react-big-scheduleresource rows, built on Ant Designopen source, MITREADME (opens in a new tab)
Wingo UI Schedulerday and week timeline plus a phone agendaPro, source copied into your projectdocs

Pick Bryntum or Syncfusion when a large team needs a full planning suite with vendor support, FullCalendar Premium when you already use FullCalendar for month views, and react-big-schedule when your app runs on Ant Design. The Wingo UI Scheduler is for a Tailwind app that wants the timeline to match the rest of its UI, work on a phone and live in the repository as source you can edit.

What does a resource timeline look like in practice?

It is one grid with every resource's day side by side, so a free slot is a gap you can see. The demo shows nine resources in three groups on a Thursday in September. The red line is now, the bar under each name is how much of its working hours is booked, and the Potomac room is in maintenance from 2 PM to 4 PM.

Drag across free time in a row to book it, drag a booking to another room or car, or stretch its edge. Drops on the Potomac room during its maintenance are refused and the bar springs back.
$ npx wingo-ui@latest add scheduler
ProScheduler docs

Tentative bookings are dashed; cancelled ones are struck through and never block a slot.

How do you set up resources and bookings?

You pass two arrays, resources for the rows and events for the bookings, plus callbacks that save what people change. Install it with npx wingo-ui@latest add scheduler. A resource is { id, name } plus optional avatar, icon, description, group, capacity, tone, color, unavailable and workingHours. A booking is a Calendar View event (id, title, start, end and optional extras) plus resourceId and an optional status.

tsx
"use client"
import { useState } from "react"
import { Car, Presentation } from "lucide-react"
import { Scheduler, type SchedulerEvent, type SchedulerResource } from "@/components/ui/scheduler"
const resources: SchedulerResource[] = [
{
id: "hudson",
name: "Hudson room",
description: "8 seats, screen",
group: "Rooms",
capacity: 8,
icon: <Presentation />,
tone: "info",
unavailable: [{ start: new Date(2026, 9, 16, 14), end: new Date(2026, 9, 16, 16), label: "Maintenance" }],
},
{ id: "emma", name: "Emma Carter", description: "Sales", group: "Team", workingHours: { start: 9, end: 17 } },
{ id: "corolla", name: "Toyota Corolla", description: "NY 12 ACM", group: "Cars", icon: <Car /> },
]
async function send(url: string, method: "POST" | "PATCH", body: unknown) {
const res = await fetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
})
if (!res.ok) throw new Error(await res.text())
return res.json()
}
export function RoomBookings({ initial }: { initial: SchedulerEvent[] }) {
const [events, setEvents] = useState(initial)
return (
<Scheduler
resources={resources}
events={events}
slotMinutes={15}
dayStartHour={7}
dayEndHour={19}
onEventChange={async (event, change) => {
await send(`/api/bookings/${event.id}`, "PATCH", change)
setEvents((list) => list.map((e) => (e.id === event.id ? { ...e, ...change } : e)))
}}
onCreateEvent={async (input) => {
const { id } = await send("/api/bookings", "POST", input)
setEvents((list) => [...list, { id, ...input }])
}}
onEventCancel={async (event) => {
await send(`/api/bookings/${event.id}`, "PATCH", { status: "cancelled" })
setEvents((list) => list.map((e) => (e.id === event.id ? { ...e, status: "cancelled" } : e)))
}}
/>
)
}

The props that shape the grid are slotMinutes (what everything snaps to), dayStartHour and dayEndHour (8 and 20 by default), slotWidth, rowHeight and resourceWidth in pixels, and height (640px) before it scrolls with the time header and names pinned.

How does drag to create, move and resize work?

React scheduler drag and drop comes down to three gestures:

  1. Drag across free slots to book. The range snaps to the slots, stops at the next booking and opens a quick-book form under it with the title focused. Enter saves, Escape cancels. Save calls onCreateEvent with { start, end, resourceId, title }.
  2. Drag a booking to move it. It follows the pointer to another time or another row, a ghost shows where it will land and turns red when that time is taken, and it settles into the slot on release.
  3. Drag an edge to resize. It stops at its neighbors instead of running over them.

The grid scrolls by itself near its edges, and Escape cancels any drag. Moves and resizes call onEventChange(event, change) with { start, end, resourceId }. The booking moves at once; return false or a rejected promise and it goes back, with the failure announced.

If you already have a booking form, pass onSlotSelect instead of onCreateEvent: a drag, click or Enter on a free slot hands you { start, end, resourceId }. onEventClick replaces the details popover the same way.

How do you stop double bookings?

Refuse overlaps in the UI and make them impossible in the database. In the UI, allowOverlap is false by default. A drop, a resize, a selection or a form save that overlaps another booking of the same resource is refused, the booking shakes and springs back, and screen readers hear "Hudson room is already booked then". Unavailable time refuses bookings even with allowOverlap on. Turn it on for people who can be double-booked; overlapping bars then stack in lanes and the row grows.

The UI check is not enough: two people can grab the same free slot at the same second. The file exports findConflicts(events, { start, end, resourceId, id? }), which returns the bookings of that resource that overlap the candidate and skips cancelled ones. Use it in your own forms on the client, and pass the booking's id when you check an edit, or the booking clashes with itself:

ts
import { findConflicts, type SchedulerEvent } from "@/components/ui/scheduler"
export function clashMessage(
events: SchedulerEvent[],
candidate: { start: Date; end: Date; resourceId: string; id?: string },
) {
const [first] = findConflicts(events, candidate)
return first ? `That time is taken by ${first.title}` : null
}

scheduler.tsx is a client module ("use client"), so do not import it into a server action or a route handler. Repeat the same rule in the database instead. In Postgres an exclusion constraint makes the overlap impossible, whatever the client sends:

sql
create extension if not exists btree_gist;
alter table bookings add constraint no_overlap
exclude using gist (resource_id with =, tstzrange(start_at, end_at) with &&)
where (status <> 'cancelled');

A write that breaks the constraint fails with exclusion_violation (SQLSTATE 23P01). Catch that code and answer with a 409. send throws on any status outside 200 to 299, so a moved booking springs back and the quick-book form shows "Could not save" plus the title (the failed label), whatever text the server sent.

How should a React booking calendar work on a phone?

On a phone it should be a list of the day's bookings with one Book button, not a grid. A day from 8 AM to 8 PM in 30 minute slots is 24 columns of 48px, about 1,150px of grid to scroll sideways on a 390px phone. So when the scheduler is narrower than 576px of its own width (a container query, so a narrow side panel gets it too and the server HTML is already right) it becomes an agenda:

  • a strip of the week's days, with dots on the busy ones,
  • the day's bookings grouped by resource with their times,
  • the resources that are free all day as chips that book them,
  • a full-width Book button that suggests the next free slot.

Bookings and the booking form open as bottom sheets. Try it in a forced phone layout:

Tap a day in the strip, tap a booking to open its sheet, or press Book to start a booking in the next free slot.
$ npx wingo-ui@latest add scheduler
ProScheduler docs

For tablets in portrait, mobileView="day" keeps the day grid, scrolling sideways with the names pinned. On touch a tap on a free slot opens the booking sheet and a long press picks a booking up to move it, so a normal swipe still scrolls; our post on dnd kit on mobile explains the same trade-off in a sortable list. Lanes are at least 56px tall on touch screens, so every booking is a 44px target, and layout="mobile" or "desktop" forces one layout. For why sheets beat popovers on phones, see bottom sheets for the web.

How do you make a resource scheduler keyboard accessible?

Make the whole grid one Tab stop and move inside it with the arrow keys, so a keyboard user does not tab through hundreds of slots. In the Wingo UI Scheduler, Left and Right move through a row's free slots and bookings, Up and Down between resources, PageUp and PageDown a day or a week. Shift with the arrows selects slots and Enter books them; Alt with the arrows moves the focused booking and Alt with Shift resizes it. Moves, resizes, new bookings and refused drops are announced in a polite live region, and reduced motion makes the settles and the shake instant.

What breaks when a resource scheduler goes to production?

Time zones, the current time and data that does not match the rows cause most of the bugs:

  • Time zones. start and end are Date objects, and hours are formatted in the zone of whatever renders them. A server on UTC and a browser in New York print different hours into the same HTML, and React reports a hydration mismatch. Store instants (timestamptz), run the server with TZ set to your users' zone, or load the scheduler with next/dynamic and ssr: false from a client component when users span zones.
  • The current time. Without defaultDate the server renders the first booking's day and the browser moves to today, so hydration matches. Pin now in tests.
  • Missing resources. A booking whose resourceId is not in resources lands in a "No resource" row. Treat that row as a bug report.
  • Working hours are whole hours. workingHours: { start: 9, end: 17 } shades the rest of the day and the weekend. Lunch breaks and one-off closures go in unavailable with a label.
  • Labels. Every string is a prop. labels={{ book: "Reserve", newBooking: "New reservation" }} changes the wording, conflict takes a function of the resource name, and DEFAULT_SCHEDULER_LABELS has the full set to spread under a translation.

When is a regular event calendar the better choice?

A regular calendar is better when people mostly look at their own meetings. For that, use Calendar View: month, week, day and agenda views with drag to reschedule. A Scheduler booking is its event plus resourceId, so the same data feeds both. Our React calendar component with events guide covers that calendar in depth, and when the rows are tasks with dependencies rather than bookings, the React Gantt chart guide fits better.

Switch between Month, Week, Day and Agenda, then drag an event to another time to reschedule it.
$ npx wingo-ui@latest add calendar-view
ProCalendar View docs

To pick one slot in a form, a Time Picker next to a date field is lighter than either. The React form components guide covers how those fields fit together, and the component guides have more single-component deep dives.

How do you add the scheduler to a Next.js app?

Run npx wingo-ui@latest add scheduler. It copies components/ui/scheduler.tsx and the 21 Wingo UI items it imports directly (plus their own dependencies), among them Calendar View (for the event type), Popover, Select, Time Picker, Tooltip and the date, motion and tone helpers, and installs lucide-react and motion. The Scheduler is part of Wingo UI Pro, so sign in first with npx wingo-ui@latest login.

Components in this post

  • Scheduler

    A React resource scheduler to book rooms, cars and people on a timeline: drag to book, move and resize, conflicts refused, agenda on phones.

    Pro
  • Calendar View

    A React event calendar for appointments and meetings: month, week, day and agenda views, a now line and drag to reschedule.

    Pro

FAQ

What is a resource scheduler in React?

It is a timeline where each row is a resource, such as a meeting room, a vehicle or a team member, and time runs across the columns. Bookings are bars in those rows, so you see availability per resource at a glance instead of one combined calendar.

Is FullCalendar's resource timeline free?

No. As of October 2026 the FullCalendar pricing page lists Timeline View and Vertical Resource View as premium features, with Premium starting at $480 for 1 to 10 developers. The Standard edition, which has the month, week and day views, is free.

How do I prevent double bookings in a React scheduler?

Refuse overlapping drops in the UI and check again on the server, because two people can book the same slot at the same moment. In the Wingo UI Scheduler allowOverlap is false by default, unavailable time always refuses bookings, and a rejected promise from onEventChange puts the booking back.

How should a booking calendar work on mobile?

Show one day as a list of bookings grouped by resource with a full-width Book button, instead of a wide grid that needs sideways scrolling. Moving a booking on touch should start with a long press, so a normal swipe still scrolls the page.

Should I use a resource scheduler or a regular calendar?

Use a resource scheduler when the question is which room, car or person is free at a given time. Use a regular event calendar with month and week views when each user mostly looks at their own appointments.

  • Scheduler
  • Booking
  • Drag and drop
  • Mobile UI

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Component guides

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.

SRSerban Rusu·Oct 19, 2026·10 min read
Component guides

Embla Carousel React: Accessible Slides, No Layout Shift

Build an embla carousel react component for photos, listings, testimonials and onboarding, with keyboard controls, pausable autoplay and no layout shift.

SRSerban Rusu·Oct 19, 2026·9 min read
Component guides

React Animated Number Counter: Rolling Odometer Digits

Build a React animated number counter that rolls each digit like an odometer, takes its separators from Intl, holds its width and respects reduced motion.

SRSerban Rusu·Oct 19, 2026·9 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • Theme generator

Resources

  • Docs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI