What You Will Learn in This Tutorial

  • How to add a React Scheduler component to your application.

  • How to define the visible date range (one year) and scroll to November 2026.

  • How to load reservation data.

  • How to change reservation color and active areas.

  • How to enable drag and drop operations (moving, resizing, creating).

  • How to show icons in the row headers.

  • How to filter rows by seat count and add a context menu to reservations.

License

Licensed for testing and evaluation purposes. Please see the license information included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript.

Generate a React Scheduler Project using an Online UI Builder App

React Scheduler Component Configurator

To get started more easily, you can use the online UI Builder app to configure the React Scheduler component and download a current React 19 + Vite starter project.

  1. Configure the component using the available preset values.

  2. Preview your changes immediately with a live Scheduler instance.

  3. Download a complete React project that includes your selected configuration.

Create a React Scheduler Component using 6 Lines of Code

react scheduler component initialization

To integrate a visual scheduler into your React application, the minimal component can look like this:

import { DayPilotScheduler } from 'daypilot-pro-react';

const Scheduler = () => {
  return <DayPilotScheduler />;
};

export default Scheduler;

This renders an empty Scheduler component with the default configuration, without any resources or reservations.

This code requires the daypilot-pro-react library, which you can install using npm:

npm install https://npm.daypilot.org/daypilot-pro-react/trial/2026.2.6907.tar.gz

You can get the latest version of daypilot-pro-react at npm.daypilot.org.

How to Load Scheduler Rows

React Scheduler Component - Loading Resources

In order to display any useful data, we need to define rows using the resources property.

The live sample keeps the row data in React state and initializes it lazily using a helper function. That gives us static sample data for the tutorial while keeping the same state shape you would use with server-loaded data later.

const createResources = () => ([
  {
    name: 'Convertible',
    id: 'G2',
    expanded: true,
    children: [
      { name: 'MINI Cooper', seats: 4, doors: 2, transmission: 'Automatic', id: 'A', image: 'Mini_Cooper_Icon_24x24.png' },
      { name: 'BMW Z4', seats: 4, doors: 2, transmission: 'Automatic', id: 'B', image: 'BMW_Z4_icon_24x24.png' },
      { name: 'Ford Mustang', seats: 4, doors: 2, transmission: 'Automatic', id: 'C', image: 'Ford_Mustang_Icon_24x24.png' },
      { name: 'Mercedes-Benz SL', seats: 2, doors: 2, transmission: 'Automatic', id: 'D', image: 'Mercedes_Benz_SL_icon_24x24.png' },
    ],
  },
  {
    name: 'SUV',
    id: 'G1',
    expanded: true,
    children: [
      { name: 'BMW X1', seats: 5, doors: 4, transmission: 'Automatic', id: 'E', image: 'BMW_X1_Icon_24x24.png' },
      { name: 'Jeep Wrangler', seats: 5, doors: 4, transmission: 'Automatic', id: 'F', image: 'Jeep_Wrangler_Icon_24x24.png' },
      { name: 'Range Rover', seats: 5, doors: 4, transmission: 'Automatic', id: 'G', image: 'Range_Rover_Icon_24x24.png' },
    ],
  },
]);

const [resources] = useState(createResources);

Depending on the specific use case, these resources could represent rooms, machines, employees, or any other entity that requires task scheduling.

How to Define the Scheduler Timeline Scale and Time Headers

React Scheduler Component - Timeline Scale and Time Headers

In the default configuration, the time header of the Scheduler displays today (one cell per hour). We will use startDatedaysscale and timeHeaders properties to customize the timeline:

<DayPilotScheduler
  startDate="2026-01-01"
  days={365}
  scale="Day"
  timeHeaders={[
    { groupBy: 'Month' },
    { groupBy: 'Day', format: 'd' },
  ]}
  treeEnabled
/>

This configuration shows the year 2026 using one cell per day.

We can also adjust the grid size using cellWidth and eventHeight properties of the React Scheduler component:

cellWidth={50}
eventHeight={35}
durationBarVisible={false}
rowMarginTop={3}
rowMarginBottom={3}

The cellWidth property defines the Scheduler grid cell width in pixels. The eventHeight property specifies the height of reservation boxes.

React Scheduler Component - Grid Cell Size

The React Scheduler component supports all properties from the JavaScript Scheduler API. In this tutorial, keeping the important values inline on <DayPilotScheduler> makes the code easier to follow than moving them into a separate config object.

How to Load Reservation Data into the React Scheduler

React Scheduler Component - Loading Reservation Data

In order to load events, the sample uses another lazy state initializer. The reservation data is fixed to November 2026, and the Scheduler scrolls to 2026-11-01 once the control is ready.

const createEvents = () => ([
  {
    id: 101,
    text: 'Reservation 101',
    start: '2026-11-02T00:00:00',
    end: '2026-11-05T00:00:00',
    resource: 'A',
  },
  {
    id: 102,
    text: 'Reservation 102',
    start: '2026-11-06T00:00:00',
    end: '2026-11-10T00:00:00',
    resource: 'A',
  },
  {
    id: 103,
    text: 'Reservation 103',
    start: '2026-11-03T00:00:00',
    end: '2026-11-10T00:00:00',
    resource: 'C',
    backColor: '#6fa8dc',
    locked: true,
  },
  {
    id: 104,
    text: 'Reservation 104',
    start: '2026-11-02T00:00:00',
    end: '2026-11-08T00:00:00',
    resource: 'E',
    backColor: '#f6b26b',
    plus: true,
  },
  {
    id: 105,
    text: 'Reservation 105',
    start: '2026-11-03T00:00:00',
    end: '2026-11-09T00:00:00',
    resource: 'G',
  },
  {
    id: 106,
    text: 'Reservation 106',
    start: '2026-11-02T00:00:00',
    end: '2026-11-07T00:00:00',
    resource: 'B',
  },
]);

const [events] = useState(createEvents);

The items of the events array need to follow the structure defined for DayPilot.Event.data.

In a real-world application, you can load the data from an API endpoint instead (use two parallel requests and wait for both of them to complete):

const loadData = async () => {
  try {
    const [resourcesResponse, eventsResponse] = await Promise.all([
      fetch('/api/resources'),
      fetch('/api/events'),
    ]);

    if (!resourcesResponse.ok || !eventsResponse.ok) {
      throw new Error('HTTP error, status = ' + resourcesResponse.status);
    }

    const resourceData = await resourcesResponse.json();
    const eventData = await eventsResponse.json();

    setResources(resourceData);
    setEvents(eventData);
  } catch (error) {
    console.error('Failed to fetch resources or events:', error);
  }
};

How to Show Additional Row Header Columns

React Scheduler Component - Row Header Columns

In this step, we will extend the Scheduler component to display the following columns in the row header:

  • Car

  • Seats

  • Doors

  • Transmission

You can define the row header columns using the rowHeaderColumns property. Each item defines the displayed column title (name), the resource field that supplies the value (display), and the initial width (width).

rowHeaderColumns={[
  { name: 'Car' },
  { name: 'Seats', display: 'seats', width: 50 },
  { name: 'Doors', display: 'doors', width: 50 },
  { name: 'Transmission', display: 'transmission', width: 90 },
]}

We will customize the row header manually and display an icon for each resource:

onBeforeRowHeaderRender={(args) => {
  if (!args.row.data.image) {
    return;
  }

  args.row.columns[0].areas = [
    {
      left: 10,
      top: 8,
      width: 24,
      height: 24,
      image: `cars/${args.row.data.image}`,
      style: 'border-radius: 50%; overflow: hidden;',
    },
  ];
}}

The same sample also includes a small seat filter in the toolbar and a reservation context menu. The toolbar calls applyFilter(), which forwards the selected value to rows.filter(), and onRowFilter decides which rows stay visible. The context menu is created using DayPilot.Menu and opened from the three-dots active area that we add in onBeforeEventRender.

const applyFilter = (value) => {
  const seats = parseInt(value, 10);
  if (!seats) {
    scheduler.rows.filter(null);
    return;
  }

  scheduler.rows.filter({ seats });
};

<div className="toolbar">
  Filter by seats:
  &nbsp;
  <select onChange={(event) => applyFilter(event.currentTarget.value)}>
    <option value="0">All</option>
    <option value="2">2</option>
    <option value="4">4</option>
    <option value="5">5</option>
  </select>
</div>
const contextMenu = new DayPilot.Menu({
  items: [
    {
      text: 'Delete',
      onClick: async (args) => {
        const modal = await DayPilot.Modal.confirm('Do you really want to delete this reservation?');
        if (modal.canceled) {
          return;
        }

        scheduler.events.remove(args.source);
      },
    },
    {
      text: 'Lock',
      onClick: (args) => {
        toggleEventLock(args.source);
      },
    },
  ],
  onShow: (args) => {
    const locked = !!args.source.data.locked;
    args.menu.items[0].disabled = locked;
    args.menu.items[1].text = locked ? 'Unlock' : 'Lock';
  },
});

<DayPilotScheduler
  contextMenu={contextMenu}
  onRowFilter={(args) => {
    if (!args.filter) {
      return;
    }

    if (args.filterParam.seats) {
      args.visible = args.row.data.seats === args.filterParam.seats;
    }
  }}
/>

How to Handle User Events (Drag and Drop)

React Scheduler Component - Handling User Events

The Scheduler defines event handlers that you can use to get notifications of user changes. Here is the exact handler used for drag and drop event moving:

onEventMoved={(args) => {
  console.log('Event moved: ', args.e.data.id, args.newStart, args.newEnd, args.newResource);
  scheduler?.message(`Event moved: ${args.e.data.text}`);
}}

With eventMoveHandling="Update", the Scheduler updates the reservation automatically. The handler just receives the notification and calls message().

How to Resize Scheduler Events using Drag and Drop

React Scheduler Component - Resizing Reservations using Drag and Drop

You can handle drag and drop event resizing using the same mechanism. The resizing is enabled by default (eventResizeHandling is set to 'Update') and the sample only reacts to the notification in onEventResized:

onEventResized={(args) => {
  console.log('Event resized: ', args.e.data.id, args.newStart, args.newEnd);
  scheduler?.message(`Event resized: ${args.e.data.text}`);
}}

How to Add New Events/Reservations

React Scheduler Component - Adding Reservations

To add support for creating new events, we need to handle onTimeRangeSelected:

onTimeRangeSelected={async (args) => {
  const modal = await DayPilot.Modal.prompt('New reservation name', 'Reservation');
  scheduler?.clearSelection();
  if (modal.canceled || !modal.result || !scheduler) {
    return;
  }

  scheduler.events.add({
    id: DayPilot.guid(),
    text: modal.result,
    start: args.start,
    end: args.end,
    resource: args.resource,
  });
}}

The event handler opens a prompt using the built-in DayPilot.Modal dialog and asks for the reservation name. The new item is added using events.add(), which applies the optimized Scheduler update directly.

React Scheduler Component - New Reservation Dialog

How to Display a Message to the User using React Scheduler API

React Scheduler Component - Display a Message

The Scheduler component offers a rich API that you can use in your application. The Scheduler methods let you use advanced features that cannot be controlled by properties alone.

The sample stores the DayPilot.Scheduler object in React state using controlRef and then uses that reference to call message() and scrollTo() directly:

const [scheduler, setScheduler] = useState(null);

useEffect(() => {
  scheduler?.scrollTo('2026-11-01');
}, [scheduler]);

<DayPilotScheduler
  // ...
  controlRef={setScheduler}
/>

This lets the sample scroll to the tutorial data once the control is ready and display small status messages after move and resize operations.

How to Customize the Color of Reservations

React Scheduler Component - Customizing Reservation Appearance

The React Scheduler component is very customizable and it lets you change the reservation color easily.

The sample hides the duration bar using durationBarVisible={false} and then customizes the rendered events in onBeforeEventRender. The same hook also adds the context-menu button and the padlock icon for locked reservations.

onBeforeEventRender={(args) => {
  if (!args.data.backColor) {
    args.data.backColor = '#93c47d';
  }

  args.data.borderColor = 'darker';
  args.data.fontColor = '#ffffff';
  args.data.areas = [
    {
      right: 4,
      top: 6,
      width: 24,
      height: 24,
      padding: 2,
      fontColor: '#ffffff',
      symbol: 'icons/daypilot.svg#threedots-v',
      action: 'ContextMenu',
      style: 'border-radius: 50%; cursor: pointer;',
    },
  ];

  if (!args.data.locked) {
    return;
  }

  args.data.areas.push({
    right: 26,
    top: 6,
    width: 24,
    height: 24,
    padding: 4,
    fontColor: '#ffffff',
    symbol: 'icons/daypilot.svg#padlock',
    style: 'border-radius: 50%; cursor: pointer;',
    onClick: async (areaArgs) => {
      const modal = await DayPilot.Modal.confirm('Do you really want to unlock this reservation?');
      if (modal.canceled) {
        return;
      }

      toggleEventLock(areaArgs.source);
    },
  });

  args.data.moveDisabled = true;
  args.data.resizeDisabled = true;
  args.data.clickDisabled = true;
  args.data.deleteDisabled = true;
}}

The special 'darker' value of the borderColor property calculates a darker border from the current backColor automatically.

Full Source Code of Scheduler.jsx

Here is the full source code of the customized React Scheduler component:

import { useEffect, useState } from 'react';
import { DayPilot, DayPilotScheduler } from 'daypilot-pro-react';

const createResources = () => [
  {
    name: 'Convertible',
    id: 'G2',
    expanded: true,
    children: [
      { name: 'MINI Cooper', seats: 4, doors: 2, transmission: 'Automatic', id: 'A', image: 'Mini_Cooper_Icon_24x24.png' },
      { name: 'BMW Z4', seats: 4, doors: 2, transmission: 'Automatic', id: 'B', image: 'BMW_Z4_icon_24x24.png' },
      { name: 'Ford Mustang', seats: 4, doors: 2, transmission: 'Automatic', id: 'C', image: 'Ford_Mustang_Icon_24x24.png' },
      { name: 'Mercedes-Benz SL', seats: 2, doors: 2, transmission: 'Automatic', id: 'D', image: 'Mercedes_Benz_SL_icon_24x24.png' },
    ],
  },
  {
    name: 'SUV',
    id: 'G1',
    expanded: true,
    children: [
      { name: 'BMW X1', seats: 5, doors: 4, transmission: 'Automatic', id: 'E', image: 'BMW_X1_Icon_24x24.png' },
      { name: 'Jeep Wrangler', seats: 5, doors: 4, transmission: 'Automatic', id: 'F', image: 'Jeep_Wrangler_Icon_24x24.png' },
      { name: 'Range Rover', seats: 5, doors: 4, transmission: 'Automatic', id: 'G', image: 'Range_Rover_Icon_24x24.png' },
    ],
  },
];

const createEvents = () => [
  {
    id: 101,
    text: 'Reservation 101',
    start: '2026-11-02T00:00:00',
    end: '2026-11-05T00:00:00',
    resource: 'A',
  },
  {
    id: 102,
    text: 'Reservation 102',
    start: '2026-11-06T00:00:00',
    end: '2026-11-10T00:00:00',
    resource: 'A',
  },
  {
    id: 103,
    text: 'Reservation 103',
    start: '2026-11-03T00:00:00',
    end: '2026-11-10T00:00:00',
    resource: 'C',
    backColor: '#6fa8dc',
    locked: true,
  },
  {
    id: 104,
    text: 'Reservation 104',
    start: '2026-11-02T00:00:00',
    end: '2026-11-08T00:00:00',
    resource: 'E',
    backColor: '#f6b26b',
    plus: true,
  },
  {
    id: 105,
    text: 'Reservation 105',
    start: '2026-11-03T00:00:00',
    end: '2026-11-09T00:00:00',
    resource: 'G',
  },
  {
    id: 106,
    text: 'Reservation 106',
    start: '2026-11-02T00:00:00',
    end: '2026-11-07T00:00:00',
    resource: 'B',
  },
];

function Scheduler() {
  const [resources] = useState(createResources);
  const [events] = useState(createEvents);
  const [scheduler, setScheduler] = useState(null);

  const toggleEventLock = (event) => {
    if (!scheduler) {
      return;
    }

    event.data.locked = !event.data.locked;
    scheduler.events.update(event);
  };

  const applyFilter = (value) => {
    if (!scheduler) {
      return;
    }

    const seats = parseInt(value, 10);
    if (!seats) {
      scheduler.rows.filter(null);
      return;
    }

    scheduler.rows.filter({ seats });
  };

  const contextMenu = new DayPilot.Menu({
    items: [
      {
        text: 'Delete',
        onClick: async (args) => {
          if (!scheduler) {
            return;
          }

          const modal = await DayPilot.Modal.confirm('Do you really want to delete this reservation?');
          if (modal.canceled) {
            return;
          }

          scheduler.events.remove(args.source);
        },
      },
      {
        text: 'Lock',
        onClick: (args) => {
          toggleEventLock(args.source);
        },
      },
    ],
    onShow: (args) => {
      const locked = !!args.source.data.locked;

      args.menu.items[1].text = locked ? 'Unlock' : 'Lock';
      args.menu.items[0].disabled = locked;
    },
  });

  useEffect(() => {
    scheduler?.scrollTo('2026-11-01');
  }, [scheduler]);

  return (
    <div>
      <div className="toolbar">
        Filter by seats:
        &nbsp;
        <select onChange={(event) => applyFilter(event.currentTarget.value)}>
          <option value="0">All</option>
          <option value="2">2</option>
          <option value="4">4</option>
          <option value="5">5</option>
        </select>
      </div>
      <DayPilotScheduler
        startDate="2026-01-01"
        days={365}
        scale="Day"
        timeHeaders={[
          { groupBy: 'Month' },
          { groupBy: 'Day', format: 'd' },
        ]}
        cellWidth={50}
        eventHeight={35}
        durationBarVisible={false}
        rowMarginTop={3}
        rowMarginBottom={3}
        treeEnabled
        timeRangeSelectedHandling="Enabled"
        eventMoveHandling="Update"
        eventResizeHandling="Update"
        rowHeaderColumns={[
          { name: 'Car' },
          { name: 'Seats', display: 'seats', width: 50 },
          { name: 'Doors', display: 'doors', width: 50 },
          { name: 'Transmission', display: 'transmission', width: 90 },
        ]}
        resources={resources}
        events={events}
        contextMenu={contextMenu}
        onBeforeRowHeaderRender={(args) => {
          if (!args.row.data.image) {
            return;
          }

          args.row.columns[0].areas = [
            {
              left: 10,
              top: 8,
              width: 24,
              height: 24,
              image: `cars/${args.row.data.image}`,
              style: 'border-radius: 50%; overflow: hidden;',
            },
          ];
        }}
        onBeforeEventRender={(args) => {
          if (!args.data.backColor) {
            args.data.backColor = '#93c47d';
          }

          args.data.borderColor = 'darker';
          args.data.fontColor = '#ffffff';
          args.data.areas = [
            {
              right: 4,
              top: 6,
              width: 24,
              height: 24,
              padding: 2,
              fontColor: '#ffffff',
              symbol: 'icons/daypilot.svg#threedots-v',
              action: 'ContextMenu',
              style: 'border-radius: 50%; cursor: pointer;',
            },
          ];

          if (args.data.locked) {
            args.data.areas.push({
              right: 26,
              top: 6,
              width: 24,
              height: 24,
              padding: 4,
              fontColor: '#ffffff',
              symbol: 'icons/daypilot.svg#padlock',
              style: 'border-radius: 50%;',
              onClick: async (areaArgs) => {
                const modal = await DayPilot.Modal.confirm('Do you really want to unlock this reservation?');
                if (modal.canceled) {
                  return;
                }

                toggleEventLock(areaArgs.source);
              },
            });

            args.data.moveDisabled = true;
            args.data.resizeDisabled = true;
            args.data.clickDisabled = true;
            args.data.deleteDisabled = true;
          }
        }}
        onRowFilter={(args) => {
          if (!args.filter) {
            return;
          }

          if (args.filterParam.seats) {
            args.visible = args.row.data.seats === args.filterParam.seats;
          }
        }}
        onEventMoved={(args) => {
          console.log('Event moved: ', args.e.data.id, args.newStart, args.newEnd, args.newResource);
          scheduler?.message(`Event moved: ${args.e.data.text}`);
        }}
        onEventResized={(args) => {
          console.log('Event resized: ', args.e.data.id, args.newStart, args.newEnd);
          scheduler?.message(`Event resized: ${args.e.data.text}`);
        }}
        onTimeRangeSelected={async (args) => {
          const modal = await DayPilot.Modal.prompt('New reservation name', 'Reservation');
          scheduler?.clearSelection();
          if (modal.canceled || !modal.result || !scheduler) {
            return;
          }

          scheduler.events.add({
            id: DayPilot.guid(),
            text: modal.result,
            start: args.start,
            end: args.end,
            resource: args.resource,
          });
        }}
        controlRef={setScheduler}
      />
    </div>
  );
}

export default Scheduler;

History

  • April 19, 2026: Rebuilt the sample on the current React 19 + Vite builder template, upgraded to daypilot-pro-react 2026.2.6907, restored the original direct Scheduler update semantics, refreshed the screenshots, and added the seat-filter/context-menu explanation.

  • July 17, 2024: Upgraded to DayPilot Pro 2024.3.5973. Added: row header column icons, row filter, event context menu.

  • May 26, 2023: Converted to a functional React component (Hooks API), React 18, DayPilot Pro for JavaScript 2023.2.5582.

  • June 21, 2021: Row columns added, how to change event color.

  • May 20, 2021: Upgraded to React 17, DayPilot Pro for JavaScript 2021.2.4990.

  • November 22, 2020: Upgraded to DayPilot Pro for JavaScript 2020.4.4766.

  • September 11, 2019: Upgraded to DayPilot Pro for JavaScript 2019.3.4000, toolbar styling, event creating, event resizing.

  • May 21, 2018: Component interop (zoom component), custom event bar color.

  • May 18, 2018: Initial version released.