Overview

  • Create a calendar/scheduler UI with a timeline displayed on the vertical axis.

  • The time cell unit can be set to a range of values, from minutes to weeks.

  • Resources (machines, rooms, cars, people) are displayed on the horizontal axis as columns.

  • The downloadable React project uses React 19 and Vite and includes a trial version of DayPilot Pro for JavaScript (see License below)

License

Licensed for testing and evaluation purposes. Please see the license agreement 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. Buy a license.

React Scheduler with Vertical and Horizontal Timeline

There is a React Scheduler component that can display a timeline on the horizontal axis and resources listed as rows:

horizontal timeline

You can display a full timeline on the vertical axis using the React Calendar component in its resource view:

vertical timeline

Show Resources as Columns in React Calendar

You can show resources as columns. To switch the React Calendar component to the resource-scheduling calendar mode, use the viewType property:

<DayPilotCalendar
  viewType={"Resources"}
  // ...
/>

The default view fills the available width. You can also use a fixed column width mode which can help with displaying a large number of columns. The calendar will display a horizontal scrollbar that lets users access all columns.

To define the resources/columns, pass an array to the columns prop. Each resource needs a unique id; an event uses the same ID in its resource property to select its column:

const [columns] = useState(() => [
  {name: "Resource A", id: "A"},
  {name: "Resource B", id: "B"},
  // ...
]);

Vertical Timeline Scale Customization

day scale

You can set the duration of the React calendar cells using the scale property.

The calendar time scale can be set to one of the predefined values:

  • Week

  • Day

  • Hour

  • Minute

  • A custom number of minutes (e.g., half days, 8-hour shifts, etc.)

The resources (such as machines, tools, devices, locations) are displayed on the horizontal axis, with each resource displaying its timeline as a column.

With a day or week scale, the time header cells match the grid cells by default. Sub-hour scales use hour headers by default. You can independently adjust the header duration using timeHeaderCellDuration.

Calendar props:

<DayPilotCalendar
  viewType={"Resources"}
  scale={"Day"}
  cellHeight={40}
  // ...
/>

Timeline Length

The React calendar supports long timelines and many resource columns. Its optimized rendering helps maintain performance; the practical limit depends on the data and browser.

To set the length of the timeline, use the days prop. This example starts at the beginning of the current month and displays all its days. A fixed-height viewport lets users scroll through the timeline without stretching the page.

Calendar props:

const [startDate] = useState(() => DayPilot.Date.today().firstDayOfMonth());
// ...
<DayPilotCalendar
  viewType={"Resources"}
  scale={"Day"}
  cellHeight={40}
  heightSpec={"Fixed"}
  height={400}
  startDate={startDate}
  days={startDate.daysInMonth()}
  // ...
/>

Snap to Grid

snap to grid

By default, the events snap to the calendar grid cells.

You can turn the snap-to-grid feature off and move the events freely during drag and drop. The sample starts with snapping disabled; use the Snap to grid checkbox to compare both modes. With useEventBoxes="Never", the event box follows its actual times instead of filling whole grid cells. With 40-pixel day cells, each vertical pixel represents 36 minutes. Snapping also affects resizing and time-range selection.

Calendar props:

const [snapToGrid, setSnapToGrid] = useState(false);
// ...
<DayPilotCalendar
  // ...
  snapToGrid={snapToGrid}
  useEventBoxes={"Never"}
  // ...
/>

Hierarchy of Resources

resource hierarchy

If you need to display a larger number of columns, you may improve the React calendar readability by organizing the columns into a tree hierarchy.

There can be one or more group levels. Select Group resources in the sample to show the People and Tools groups. Set headerLevels to 2 to display both header rows. The leaf IDs stay the same as in the flat layout, so existing events remain assigned to the same resources.

  • The hierarchy can correspond to the physical arrangement of the resources (building, room, rack).

  • You can also use the tree to group resources by type (cars, tools, people).

Calendar props:

const [groupedColumns] = useState(() => [
  {name: "People", children: [
    {name: "John", id: "A"},
    {name: "Mary", id: "B"}
  ]},
  {name: "Tools", children: [
    {name: "Hammer", id: "C"},
    {name: "Saw", id: "D"},
    {name: "Screwdriver", id: "E"},
    // ...
  ]}
]);
// ...
<DayPilotCalendar
  // ...
  columns={grouped ? groupedColumns : columns}
  headerLevels={grouped ? 2 : 1}
  // ...
/>

Full Source Code

This is the source code of src/scheduler/VerticalScheduler.jsx, which shows resources as columns and a vertical timeline. React supplies the initial columns and events. Selecting an empty range opens a prompt; after confirmation, calendar.events.add() adds the event to the live control. Drag an event to change its time or resource, or drag its top/bottom edge to resize it. The colored circles show each event owner. Changes are held in browser memory and reset when you reload the page.

import { useRef, useState } from "react";
import { DayPilot, DayPilotCalendar } from "daypilot-pro-react";

const VerticalScheduler = () => {
  const calendarRef = useRef(null);
  const [startDate] = useState(() => DayPilot.Date.today().firstDayOfMonth());
  const [snapToGrid, setSnapToGrid] = useState(false);
  const [grouped, setGrouped] = useState(false);
  const [columns] = useState(() => [
    {name: "Resource A", id: "A"},
    {name: "Resource B", id: "B"},
    {name: "Resource C", id: "C"},
    {name: "Resource D", id: "D"},
    {name: "Resource E", id: "E"},
    {name: "Resource F", id: "F"},
    {name: "Resource G", id: "G"}
  ]);
  const [groupedColumns] = useState(() => [
    {name: "People", children: [
      {name: "John", id: "A"},
      {name: "Mary", id: "B"}
    ]},
    {name: "Tools", children: [
      {name: "Hammer", id: "C"},
      {name: "Saw", id: "D"},
      {name: "Screwdriver", id: "E"},
      {name: "Wrench", id: "F"},
      {name: "Drill", id: "G"}
    ]}
  ]);
  const [events] = useState(() => [
    {
      id: 1,
      text: "Event 1",
      start: startDate.addDays(1),
      end: startDate.addDays(4),
      resource: "A",
      owner: "AB"
    },
    {
      id: 2,
      text: "Event 2",
      start: startDate.addDays(2),
      end: startDate.addDays(4),
      resource: "C",
      barColor: "#5bbe2d",
      barBackColor: "#93c47d",
      owner: "BK"
    },
    {
      id: 3,
      text: "Event 3",
      start: startDate.addDays(1),
      end: startDate.addDays(3),
      resource: "D",
      barColor: "#f1c232",
      barBackColor: "#f1c232",
      owner: "FH"
    },
    {
      id: 4,
      text: "Event 4",
      start: startDate.addDays(1),
      end: startDate.addDays(3),
      resource: "E",
      barColor: "#cc4125",
      barBackColor: "#ea9999",
      owner: "IG"
    }
  ]);

  const onTimeRangeSelected = async (args) => {
    const calendar = args.control;
    const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
    calendar.clearSelection();
    if (modal.canceled) {
      return;
    }
    calendar.events.add({
      start: args.start,
      end: args.end,
      id: DayPilot.guid(),
      resource: args.resource,
      text: modal.result,
      owner: "AB"
    });
  };

  const onBeforeEventRender = (args) => {
    if (!args.data.barColor) {
      args.data.barColor = "#36b7ff";
    }
    if (!args.data.owner) {
      args.data.owner = "??";
    }
    args.data.areas = [{
      bottom: 5,
      right: 10,
      width: 28,
      height: 28,
      action: "None",
      backColor: args.data.barColor,
      fontColor: "#fff",
      padding: 0,
      horizontalAlignment: "center",
      verticalAlignment: "center",
      borderRadius: "50%",
      cssClass: "event-owner",
      text: args.data.owner,
      style: "border: 2px solid #fff; font-size: 16px; line-height: 1;"
    }];
  };

  return (
    <div>
      <div className="toolbar" role="group" aria-label="Timeline options">
        <label>
          <input type="checkbox" checked={snapToGrid}
            onChange={(e) => setSnapToGrid(e.target.checked)} />
          Snap to grid
        </label>
        <label>
          <input type="checkbox" checked={grouped}
            onChange={(e) => setGrouped(e.target.checked)} />
          Group resources
        </label>
      </div>
      <DayPilotCalendar
        viewType={"Resources"}
        scale={"Day"}
        cellHeight={40}
        heightSpec={"Fixed"}
        height={400}
        startDate={startDate}
        days={startDate.daysInMonth()}
        columns={grouped ? groupedColumns : columns}
        headerLevels={grouped ? 2 : 1}
        snapToGrid={snapToGrid}
        useEventBoxes={"Never"}
        events={events}
        onTimeRangeSelected={onTimeRangeSelected}
        onBeforeEventRender={onBeforeEventRender}
        ref={calendarRef}
      />
    </div>
  );
};

export default VerticalScheduler;