Overview

  • Create a monthly calendar UI in Next.js using the React monthly calendar component.

  • Use the React Hooks API to implement the functionality (load task data, define event handlers).

  • Customize the appearance of tasks and add a progress bar to the task body.

  • The Next.js project includes the open-source DayPilot Lite for JavaScript scheduling library.

License

Apache License 2.0

Creating a Monthly Calendar UI in Next.js

Creating a Monthly Calendar UI in Next.js

We will start with the minimum steps required to start using the React monthly calendar component in in Next.js.

1. Install DayPilot Lite for React

Begin by installing the DayPilot Lite for React library using npm:

npm install @daypilot/daypilot-lite-react

2. Enable Client-Side Rendering

Since the DayPilot Month component is a client-side component, you need to enable client-side rendering in your Next.js component by adding 'use client'; at the top of your file.

3. Import the DayPilotMonth Component

Import the DayPilotMonth component from the @daypilot/daypilot-lite-react package:

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

4. Add the DayPilotMonth Component to Your JSX

Insert the <DayPilotMonth /> tag into your component's JSX to render the calendar:

'use client';

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

export default function Calendar() {

    return (
      <DayPilotMonth />
    )
}

Setting the Visible Month in the Next.js Calendar

Setting the Visible Month in the Next.js Calendar

Now we can configure the monthly calendar component. In this step, we will set the visible month of the Next.js monthly calendar.

1.Initialize the startDate State Variable

To control the visible month, we’ll create a state variable called startDate. This state will determine which month is displayed when the calendar first renders. If you change this variable later, the calendar will update to show that month.

import { useState } from 'react';
import { DayPilot } from '@daypilot/daypilot-lite-react';

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

2. Pass startDate to the DayPilotMonth Component

Next, pass the startDate state variable as a prop to the DayPilotMonth component. This tells the calendar which month to display upon loading.

<DayPilotMonth startDate={startDate} />

By setting the startDate prop, the calendar will display November 2025.

Adding and Managing Events in the Monthly Calendar

Adding and Managing Events in the Next.js Monthly Calendar

To load event data into the calendar, we'll create a new events state variable and use the setEvents() function within the useEffect() hook to load the data when the component mounts.

This example uses a simple local array of events. In your Next.js application, you can replace it with a server-side HTTP request to fetch events from a database or API.

1. Initialize the events State Variable

Begin by initializing the events state variable:

const [events, setEvents] = useState<DayPilot.EventData[]>([]);

2. Load Event Data on Startup

Use the useEffect() hook to load the event data when the component mounts:

useEffect(() => {
  const data: DayPilot.EventData[] = [
    {
      id: 1,
      text: "Task 1",
      start: "2025-11-05T10:30:00",
      end: "2025-11-05T13:00:00",
      tags: { progress: 60 },
    },
    // Additional events...
  ];
  setEvents(data);
}, []);

Event properties:

  • id: A unique identifier for the event (required).

  • start and end: The start and end times of the event (required).

  • text: The text displayed on the event.

  • tags: An object to store custom data associated with the event, such as progress indicators or custom colors.

See also DayPilot.Event.data API docs for a full list of available event properties.

3. Update the DayPilotMonth Component

Next, update the <DayPilotMonth /> component in your JSX to include the events prop:

<DayPilotMonth startDate={startDate} events={events} />

By adding the events prop, the calendar will display the events you've defined in your state variable.

Displaying Progress Bars in Events

Displaying Progress Bars in Events in Next.js Monthly Calendar

We'll use the onBeforeEventRender event handler to customize the appearance of events. By defining custom active areas within each event, we can display progress bars that visually represent the progress value stored in each event's tags.

1. Define Progress Values in Event Data

First, ensure that your event data includes a progress value within the tags property. This value should be a number between 0 and 100 representing the percentage of completion.

const data: DayPilot.EventData[] = [
  {
    id: 1,
    text: "Task 1",
    start: "2025-11-05T10:30:00",
    end: "2025-11-05T13:00:00",
    tags: { progress: 60 },
  },
  {
    id: 2,
    text: "Task 2",
    start: "2025-11-06T09:30:00",
    end: "2025-11-06T11:30:00",
    tags: { progress: 80 },
  },
  // ...
];

2. Implement the onBeforeEventRender Handler

The onBeforeEventRender event handler allows you to customize how events are rendered on the Next.js calendar. We'll use this handler to add custom areas to each event for displaying the progress bar.

<DayPilotMonth
  startDate={startDate}
  events={events}
  onBeforeEventRender={onBeforeEventRender}
  // ...
/>

3. Define the onBeforeEventRender Function

Implement the onBeforeEventRender function to customize the event rendering. We'll extract the progress value from the event's tags and define custom areas within the event to display the progress bar and other information.

const onBeforeEventRender = (args: DayPilot.MonthBeforeEventRenderArgs) => {
  // extract the color from tags or use a default
  const color = args.data.tags?.color || "#3d85c6";
  args.data.backColor = color + "cc"; // Adding transparency

  // extract the progress value or default to 0
  const progress = args.data.tags?.progress || 0;

  // clear default event HTML
  args.data.html = "";

  // define custom areas within the event
  args.data.areas = [
    // event text area
    {
      id: "text",
      top: 5,
      left: 5,
      right: 5,
      height: 20,
      text: args.data.text,
      fontColor: "#fff",
    },
    // progress percentage text
    {
      id: "progress-text",
      bottom: 5,
      left: 5,
      right: 5,
      height: 20,
      text: `${progress}%`,
      fontColor: "#000",
      backColor: "#ffffff33",
      style: "text-align: center; line-height: 20px;",
    },
    // background for progress bar
    {
      id: "progress-background",
      bottom: 10,
      left: 10,
      right: 10,
      height: 10,
      borderRadius: "5px",
      backColor: "#ffffff33",
      toolTip: `Progress: ${progress}%`,
    },
    // actual progress bar
    {
      id: "progress-bar",
      bottom: 10,
      left: 10,
      width: `calc((100% - 20px) * ${progress / 100})`,
      height: 10,
      borderRadius: "5px",
      backColor: color,
    },
    // ...
  ];
};

The actual CSS width of the "progress-bar" active area is calculated from the progress value using a CSS calc() function.

Editing Calendar Events with Modal Forms

Editing Calendar Events with Modal Forms in Next.js

Now, we'll implement functionality to edit events in your Next.js monthly calendar using modal forms. This feature allows users to modify event details such as the title, color, and progress percentage.

1. Implement the editEvent Function

Create a function called editEvent that will be responsible for displaying the modal form and handling the event update.

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);
};

In the editEvent() function, we'll use the DayPilot.Modal.form() method to display a modal dialog containing a form with input fields.

There modal dialog displays three fields:

  • Event text field which is a standard text input field.

  • Event color drop-down list which lets you select a color from the colors list.

  • A drop-down list that defines the progress percentage.

The colors are defined as follows:

const colors = [
    {name: "Green", id: "#6aa84f"},
    {name: "Blue", id: "#3d85c6"},
    {name: "Turquoise", id: "#00aba9"},
    // ...
];

And here is the list of predefined progress levels:

const progressValues = [
    {name: "0%", id: 0},
    {name: "10%", id: 10},
    {name: "20%", id: 20},
    // ...
];

2. Attach the editEvent Function to Event Clicks

To allow users to edit events by clicking on them, update the onEventClick handler in your DayPilotMonth component.

<DayPilotMonth
  // ...
  onEventClick={async (args) => {
    await editEvent(args.e);
  }}
/>

3. Integrate a Context Menu for Events

We'll also add a context menu to provide additional options when users right-click on an event.

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

Attach the context menu to the calendar:

<DayPilotMonth
  // ...
  contextMenu={contextMenu}
/>

4. Ensure the Calendar Reference is Set

We need to make sure we have a reference to the calendar control to update events.

We will add a calendar state variable:

const [calendar, setCalendar] = useState<DayPilot.Month>();

The controlRef prop allows us to store a reference to the calendar instance in the calendar state variable.

<DayPilotMonth
  // Other props...
  controlRef={setCalendar}
/>

Full Source Code

And here is the full source code of our Next.js monthly calendar component:

'use client';

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

export default function Calendar() {

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

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

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

    const colors = [
        {name: "Green", id: "#6aa84f"},
        {name: "Blue", id: "#3d85c6"},
        {name: "Turquoise", id: "#00aba9"},
        {name: "Light Blue", id: "#56c5ff"},
        {name: "Yellow", id: "#f1c232"},
        {name: "Orange", id: "#e69138"},
        {name: "Red", id: "#cc4125"},
        {name: "Light Red", id: "#ff0000"},
        {name: "Purple", id: "#af8ee5"},
    ];

    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 onBeforeEventRender = (args: DayPilot.MonthBeforeEventRenderArgs) => {
        const color = args.data.tags && args.data.tags.color || "#3d85c6";
        args.data.backColor = color + "cc";

        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,
                symbol: "icons/daypilot.svg#minichevron-down-2",
                fontColor: "#fff",
                backColor: "#00000033",
                style: "border-radius: 25%; cursor: pointer;",
                toolTip: "Show context menu",
                action: "ContextMenu",
            },
        ];
    };

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

    useEffect(() => {

        if (!calendar || calendar.disposed()) {
            return;
        }
        const events: DayPilot.EventData[] = [
            {
                id: 1,
                text: "Task 1",
                start: "2025-11-05T10:30:00",
                end: "2025-11-05T13:00:00",
                tags: {
                    progress: 60,
                }
            },
            {
                id: 2,
                text: "Task 2",
                start: "2025-11-06T09:30:00",
                end: "2025-11-06T11:30:00",
                tags: {
                    color: "#6aa84f",
                    progress: 100,
                }
            },
            {
                id: 3,
                text: "Task 3",
                start: "2025-11-13T12:00:00",
                end: "2025-11-13T15:00:00",
                tags: {
                    color: "#f1c232",
                    progress: 20,
                }
            },
            {
                id: 4,
                text: "Task 4",
                start: "2025-11-04T11:30:00",
                end: "2025-11-04T14:30:00",
                tags: {
                    color: "#e69138",
                    progress: 50,
                }
            },
        ];

        setEvents(events);

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

    }, [calendar, datePicker]);

    const onTimeRangeSelected = async (args: DayPilot.MonthTimeRangeSelectedArgs) => {
        const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
        calendar?.clearSelection();
        if (modal.canceled) {
            return;
        }
        // console.log("modal.result", modal.result, calendar);
        calendar?.events.add({
            start: args.start,
            end: args.end,
            id: DayPilot.guid(),
            text: modal.result,
            tags: {}
        });
    };

    return (
        <div style={styles.wrap}>
            <div style={styles.left}>
                <DayPilotNavigator
                    selectMode={"Month"}
                    showMonths={3}
                    skipMonths={3}
                    onTimeRangeSelected={args => setStartDate(args.start)}
                    controlRef={setDatePicker}
                    />
            </div>
            <div style={styles.main}>
                <div className={"toolbar"}>
                    <button onClick={onTodayClick}>Today</button>
                </div>
                <DayPilotMonth
                    startDate={startDate}
                    events={events}
                    eventBorderRadius={"5px"}
                    eventBarVisible={false}
                    eventHeight={80}
                    cellHeight={120}
                    onTimeRangeSelected={onTimeRangeSelected}
                    onEventClick={async args => { await editEvent(args.e); }}
                    contextMenu={contextMenu}
                    onBeforeEventRender={onBeforeEventRender}
                    controlRef={setCalendar}
                />
            </div>
        </div>
    )
}

You can download the complete Next.js project using the download link at the top of this tutorial.