Overview

  • How to add the React Calendar component from the open-source DayPilot package to your Next.js application.

  • How to configure the calendar to display multiple resources as columns.

  • How to display a context menu icon in the column header that displays a context menu with additional resource actions (Edit, Delete, Block).

  • How to change the current date using Next/Previous/Today buttons and a date picker.

  • Download the Next.js 15 application with full source code.

License

Apache License 2.0

Creating a Resource-Scheduling Calendar in Next.js

We will use the React package from DayPilot Lite, the open-source library of calendar and scheduling components.

npm install @daypilot/daypilot-lite-react

It includes the React Calendar component that we will use to create the resource-scheduling calendar UI in our Next.js application.

It’s a client-side component, so we need to mark our ResourceCalendar component as such:

'use client';

Import the React calendar component (DayPilotCalendar):

import {DayPilotCalendar} from "@daypilot/daypilot-lite-react";

Now, we can define the ResourceCalendar component:

export default function ResourceCalendar() {
    return (
        <DayPilotCalendar
            viewType={"Resources"}
        />
    )
}

The viewType property switches the React calendar to the resources view, which displays resources as columns. In the next step, you will see how to define the columns.

Displaying Resources as Columns in the Next.js Scheduling Calendar

Display Resources as Columns in the Open-Source Next.js Scheduling Calendar

To define the resources for the calendar, we use a columns state variable. It stores an array of data items that use the structure defined using the DayPilot.CalendarColumnData TypeScript interface (see also the columns prop API docs).

const [columns, setColumns] = useState<DayPilot.CalendarColumnData[]>([]);

The interface is very simple, it requires two properties to be defined:

  • id - identifies the resources and is used to match the assigned tasks/events

  • name - the resource name that will be displayed in the column header

In our resource calendar component, we will load the resource data in a useEffect() block on startup:

useEffect(() => {

    if (!calendar || calendar.disposed()) {
        return;
    }

    const columns: DayPilot.CalendarColumnData[] = [
        {name: "Resource 1", id: "R1"},
        {name: "Resource 2", id: "R2"},
        {name: "Resource 3", id: "R3"},
        {name: "Resource 4", id: "R4"},
        {name: "Resource 5", id: "R5"},
        {name: "Resource 6", id: "R6"},
        {name: "Resource 7", id: "R7"},
        {name: "Resource 8", id: "R8"},
    ];
    setColumns(columns);

}, [calendar]);

We need to tell the calendar to display these resources using the columns prop:

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

Any change made to the columns state variable using the setColumns() will automatically trigger an update of the Calendar columns. We will use this later when marking the resources as blocked.

Setting the Date in the Next.js Resource Calendar

Setting the Date in the Next.js Resource Calendar

Each column shows data for one day, with hours displayed on the vertical axis.

The Calendar component uses the value of the startDate property to set the column date. By default, it is set to today. In our Next.js app, we will let users select a custom date using a date picker, so we need to set the startDate value dynamically.

First, we create a startDate state variable:

const [startDate, setStartDate] = useState<string|DayPilot.Date>(DayPilot.Date.today());

The startDate prop of the React Calendar accepts either a DayPilot.Date object, or a string in ISO 8601 format (yyyy-mm-dd).

To set a specific initial date, we can also use a string:

const [startDate, setStartDate] = useState<string|DayPilot.Date>("2025-11-05");

Now we point the startDate prop of the React Calendar to our startDate state variable:

<DayPilotCalendar
   startDate={startDate}
/>

Note: Instead of using the same startDate for all columns, you can define the date for each column independently. This approach allows you to create specialized scheduling views with non-consecutive days, or views displaying multiple days for each resource.

The startDate state variable lets us switch the date easily. Here is how you can add Next and Previous buttons to the toolbar:

<div className={"toolbar"}>
    <button onClick={onPreviousClick} className={"btn-light"}>Previous</button>
    <button onClick={onTodayClick}>Today</button>
    <button onClick={onNextClick} className={"btn-light"}>Next</button>
</div>

The event handlers (onTodayClick, onPreviousClick, onNextClick) simply set the new value:

const onTodayClick = () => {
    datePicker?.select(DayPilot.Date.today());
};

const onPreviousClick = () => {
    const previous = new DayPilot.Date(startDate).addDays(-1);
    datePicker?.select(previous);
};

const onNextClick = () => {
    const next = new DayPilot.Date(startDate).addDays(1);
    datePicker?.select(next);
};

To load task data for the newly selected date, you can add a useEffect() block that watches the startDate changes:

useEffect(() => {
  const taskData = [ ... ];
  setEvents(taskData);
}, [startDate]);

Adding Context Menu to Resources/Columns

Adding Context Menu to Columns in the Next.js Resource Calendar

As a example of the API that you can use to customize the Calendar component, we will define a context menu for the resources. We will also add an icon to the right side of the column headers that will display an icon (three dots in a circle), that will provide a context menu hint to the users.

The onBeforeHeaderRender lets you define properties of each column header. We will use it to add an active area with the icons that open a context menu on click.

const onBeforeHeaderRender = (args: DayPilot.CalendarBeforeHeaderRenderArgs) => {
    args.header.areas = [
        {
            right: 5,
            top: "calc(50% - 10px)",
            width: 20,
            height: 20,
            action: "ContextMenu",
            symbol: "icons/daypilot.svg#threedots-v",
            style: "cursor: pointer",
            toolTip: "Show context menu",
            borderRadius: "50%",
            backColor: "#00000033",
            fontColor: "#ffffff",
            padding: 2,
            menu: new DayPilot.Menu({
                onShow: async args => {
                    const column = columns.find(c => c.id === args.source.id);
                    const items = args.menu.items || [];
                    if (column?.blocked) {
                        items[0].text = "Unblock";
                    }
                    else {
                        items[0].text = "Block";
                    }
                },
                items: [
                    {
                        text: "Block",
                        onClick: async ({ source }) => {
                            const updatedColumns = columns.map(c =>  c.id === source.id ? { ...c, blocked: !c.blocked } : c);
                            setColumns(updatedColumns);
                        }
                    }
                ]
            })
        }
    ];
    args.header.horizontalAlignment = "left";
};

Blocking Drag and Drop Actions for Selected Columns

Blocking Drag and Drop Actions for Selected Resources in the Next.js Resource Calendar

The Pro version supports marking cells as disabled, which prevents the drag-and-drop operations with real-time feedback. That is not available in the Lite version, but you can still forbid certain operations based on custom rules.

In this chapter, we are going to cancel the drag-and-drop operations for the selected columns.

To mark the columns as blocked, we need to store the blocked status in the column data object. The TypeScript interface (DayPilot.CalendarColumnData) only defines the required properties. So in order to support custom properties, we need to define a custom class (ColumnData) that implements the interface:

class ColumnData implements DayPilot.CalendarColumnData {
    id: string = "";
    name: string = "";
    blocked?: boolean;
}

Now we can draw cells of the blocked resources in darker color:

const onBeforeCellRender = (args: DayPilot.CalendarBeforeCellRenderArgs) => {
    const column = columns.find(c => c.id === args.cell.resource);
    if (column?.blocked) {
        args.cell.properties.backColor = "#f0f0f0";
    }
};

// ...

<DayPilotCalendar
  onBeforeCellRender={onBeforeCellRender}
/>

To disable creation of new events for the blocked resources, we add a condition to the onTimeRangeSelected event handler that checks the resource status before adding a new task.

const onTimeRangeSelected = async (args: DayPilot.CalendarTimeRangeSelectedArgs) => {

    const column = columns.find(c => c.id === args.resource);
    if (column?.blocked) {
        calendar?.clearSelection();
        return;
    }

    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(),
        text: modal.result,
        resource: args.resource,
        tags: {}
    });
};

// ...

<DayPilotCalendar
  onTimeRangeSelected={onTimeRangeSelected}
/>

To prevent moving existing tasks to a blocked resource, we add a similar check to the onEventMove event hanlder, which is fired before the task is actually moved in the Calendar UI.

If the target resource is blocked, we cancel the move by calling args.preventDefault().

const onEventMove = async (args: DayPilot.CalendarEventMoveArgs) => {
    const column = columns.find(c => c.id === args.newResource);
    if (column?.blocked) {
        args.preventDefault();
    }
};

// ...

<DayPilotCalendar
  onEventMove={onEventMove}
/>

Full Source Code

Below is the complete TypeScript source code for our Next.js resource-scheduling calendar component, which shows planned tasks together with their progress, laid out in columns representing resources.

'use client';

import React, {useEffect, useState} from "react";
import {DayPilot, DayPilotCalendar, DayPilotNavigator} from "@daypilot/daypilot-lite-react";
import "./toolbar.css";

class ColumnData implements DayPilot.CalendarColumnData {
    id: string = "";
    name: string = "";
    blocked?: boolean;
}

export default function ResourceCalendar() {

    const [calendar, setCalendar] = useState<DayPilot.Calendar>();
    const [datePicker, setDatePicker] = useState<DayPilot.Navigator>();

    const [events, setEvents] = useState<DayPilot.EventData[]>([]);
    const [columns, setColumns] = useState<ColumnData[]>([]);
    const [startDate, setStartDate] = useState<string|DayPilot.Date>("2025-11-04");

    const styles = {
        wrap: {
            display: "flex"
        },
        left: {
            marginRight: "10px"
        },
        main: {
            flexGrow: "1"
        }
    };

    const colors = [
        { name: "Dark Green", id: "#228B22" },
        { name: "Green", id: "#6aa84f" },
        { name: "Yellow", id: "#f1c232" },
        { name: "Orange", id: "#e69138" },
        { name: "Crimson", id: "#DC143C" },
        { name: "Light Coral", id: "#F08080" },
        { name: "Purple", id: "#9370DB" },
        { name: "Turquoise", id: "#40E0D0" },
        { name: "Light Blue", id: "#ADD8E6" },
        { name: "Sky Blue", id: "#87CEEB" },
        { name: "Blue", id: "#3d85c6" },
    ];

    const progressValues = [
        {name: "0%", id: 0},
        {name: "10%", id: 10},
        {name: "20%", id: 20},
        {name: "30%", id: 30},
        {name: "40%", id: 40},
        {name: "50%", id: 50},
        {name: "60%", id: 60},
        {name: "70%", id: 70},
        {name: "80%", id: 80},
        {name: "90%", id: 90},
        {name: "100%", id: 100},
    ];

    const editEvent = async (e: DayPilot.Event) => {
        const form = [
            {name: "Event text", id: "text", type: "text"},
            {name: "Event color", id: "tags.color", type: "select", options: colors},
            {name: "Progress", id: "tags.progress", type: "select", options: progressValues },
        ];

        const modal = await DayPilot.Modal.form(form, e.data);
        if (modal.canceled) { return; }

        const updatedEvent = modal.result;

        calendar?.events.update(updatedEvent);
    };

    const contextMenu = new DayPilot.Menu({
        items: [
            {
                text: "Delete",
                onClick: async args => {
                    calendar?.events.remove(args.source);
                },
            },
            {
                text: "-"
            },
            {
                text: "Edit...",
                onClick: async args => {
                    await editEvent(args.source);
                }
            }
        ]
    });

    const onBeforeHeaderRender = (args: DayPilot.CalendarBeforeHeaderRenderArgs) => {
        args.header.areas = [
            {
                right: 5,
                top: "calc(50% - 10px)",
                width: 20,
                height: 20,
                action: "ContextMenu",
                symbol: "icons/daypilot.svg#threedots-v",
                style: "cursor: pointer",
                toolTip: "Show context menu",
                borderRadius: "50%",
                backColor: "#00000033",
                fontColor: "#ffffff",
                padding: 2,
                menu: new DayPilot.Menu({
                    onShow: async args => {
                        const column = columns.find(c => c.id === args.source.id);
                        const items = args.menu.items || [];
                        if (column?.blocked) {
                            items[0].text = "Unblock";
                        }
                        else {
                            items[0].text = "Block";
                        }
                    },
                    items: [
                        {
                            text: "Block",
                            onClick: async (args) => {
                                const updatedColumns = columns.map(c =>  c.id === args.source.id ? { ...c, blocked: !c.blocked } : c);
                                setColumns(updatedColumns);
                            }
                        },
                        {
                            text: "Edit",
                            onClick: async (args) => {
                                const column = columns.find(c => c.id === args.source.id);
                                if (!column) {
                                    return;
                                }
                                const modal = await DayPilot.Modal.prompt("Edit column name:", column.name);
                                if (modal.canceled) {
                                    return;
                                }
                                const updatedColumns = columns.map(c =>  c.id === args.source.id ? { ...c, name: modal.result } : c);
                                setColumns(updatedColumns);
                            }
                        },
                        {
                            text: "Delete",
                            onClick: async (args) => {
                                const updatedColumns = columns.filter(c => c.id !== args.source.id);
                                setColumns(updatedColumns);
                            }
                        }
                    ]
                })
            }
        ];
    };

    const onBeforeCellRender = (args: DayPilot.CalendarBeforeCellRenderArgs) => {
        const column = columns.find(c => c.id === args.cell.resource);
        if (column?.blocked) {
            args.cell.properties.backColor = "#f0f0f0";
        }
    };

    const onBeforeEventRender = (args: DayPilot.CalendarBeforeEventRenderArgs) => {
        const color = args.data.tags && args.data.tags.color || "#3d85c6";
        args.data.backColor = color + "cc";
        args.data.borderColor = "darker";

        const progress = args.data.tags?.progress || 0;

        args.data.html = "";

        args.data.areas = [
            {
                id: "text",
                top: 5,
                left: 5,
                right: 5,
                height: 20,
                text: args.data.text,
                fontColor: "#fff",
            },
            {
                id: "progress-text",
                bottom: 5,
                left: 5,
                right: 5,
                height: 40,
                text: progress + "%",
                borderRadius: "5px",
                fontColor: "#000",
                backColor: "#ffffff33",
                style: "text-align: center; line-height: 20px;",
            },
            {
                id: "progress-background",
                bottom: 10,
                left: 10,
                right: 10,
                height: 10,
                borderRadius: "5px",
                backColor: "#ffffff33",
                toolTip: "Progress: " + progress + "%",
            },
            {
                id: "progress-bar",
                bottom: 10,
                left: 10,
                width: `calc((100% - 20px) * ${progress / 100})`,
                height: 10,
                borderRadius: "5px",
                backColor: color,
            },
            {
                id: "menu",
                top: 5,
                right: 5,
                width: 20,
                height: 20,
                padding: 2,
                symbol: "icons/daypilot.svg#threedots-v",
                fontColor: "#fff",
                backColor: "#00000033",
                borderRadius: "50%",
                style: "cursor: pointer;",
                toolTip: "Show context menu",
                action: "ContextMenu",
            },
        ];
    };

    const onTodayClick = () => {
        datePicker?.select(DayPilot.Date.today());
    };

    const onPreviousClick = () => {
        const previous = new DayPilot.Date(startDate).addDays(-1);
        datePicker?.select(previous);
    };

    const onNextClick = () => {
        const next = new DayPilot.Date(startDate).addDays(1);
        datePicker?.select(next);
    };

    useEffect(() => {

        if (!calendar || calendar.disposed()) {
            return;
        }

        const columns: ColumnData[] = [
            {name: "Resource 1", id: "R1"},
            {name: "Resource 2", id: "R2"},
            {name: "Resource 3", id: "R3"},
            {name: "Resource 4", id: "R4"},
            {name: "Resource 5", id: "R5"},
            {name: "Resource 6", id: "R6"},
            {name: "Resource 7", id: "R7"},
            {name: "Resource 8", id: "R8"},
        ];
        setColumns(columns);

        const events: DayPilot.EventData[] = [
            {
                id: 1,
                text: "Task 1",
                start: "2025-11-04T10:30:00",
                end: "2025-11-04T16:00:00",
                resource: "R1",
                tags: {
                    progress: 60,
                }
            },
            {
                id: 2,
                text: "Task 2",
                start: "2025-11-04T09:30:00",
                end: "2025-11-04T11:30:00",
                resource: "R2",
                tags: {
                    color: "#6aa84f",
                    progress: 100,
                }
            },
            {
                id: 3,
                text: "Task 3",
                start: "2025-11-04T12:00:00",
                end: "2025-11-04T15:00:00",
                resource: "R2",
                tags: {
                    color: "#f1c232",
                    progress: 30,
                }
            },
            {
                id: 4,
                text: "Task 4",
                start: "2025-11-04T11:30:00",
                end: "2025-11-04T14:30:00",
                resource: "R3",
                tags: {
                    color: "#e69138",
                    progress: 60,
                }
            },
        ];

        setEvents(events);

        datePicker?.select("2025-11-04");

    }, [calendar, datePicker]);

    const onTimeRangeSelected = async (args: DayPilot.CalendarTimeRangeSelectedArgs) => {

        const column = columns.find(c => c.id === args.resource);
        if (column?.blocked) {
            calendar?.clearSelection();
            return;
        }

        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(),
            text: modal.result,
            resource: args.resource,
            tags: {}
        });
    };

    const onEventMove = async (args: DayPilot.CalendarEventMoveArgs) => {
        const column = columns.find(c => c.id === args.newResource);
        if (column?.blocked) {
            args.preventDefault();
        }
    };

    return (
        <div style={styles.wrap}>
            <div style={styles.left}>
                <DayPilotNavigator
                    selectMode={"Day"}
                    showMonths={3}
                    skipMonths={3}
                    onTimeRangeSelected={args => setStartDate(args.start)}
                    controlRef={setDatePicker}
                    />
            </div>
            <div style={styles.main}>
                <div className={"toolbar"}>
                    <button onClick={onPreviousClick} className={"btn-light"}>Previous</button>
                    <button onClick={onTodayClick}>Today</button>
                    <button onClick={onNextClick} className={"btn-light"}>Next</button>
                </div>
                <DayPilotCalendar
                    viewType={"Resources"}
                    columns={columns}
                    startDate={startDate}
                    events={events}
                    eventBorderRadius={"5px"}
                    headerHeight={50}
                    durationBarVisible={false}
                    onTimeRangeSelected={onTimeRangeSelected}
                    onEventClick={async args => { await editEvent(args.e); }}
                    contextMenu={contextMenu}
                    onBeforeHeaderRender={onBeforeHeaderRender}
                    onBeforeEventRender={onBeforeEventRender}
                    onBeforeCellRender={onBeforeCellRender}
                    onEventMove={onEventMove}
                    controlRef={setCalendar}
                />
            </div>
        </div>
    )
}

You can download the complete Next.js projet using the “Download” link at the top of the article.