
| Demo | |
|---|---|
| Downloads | |
| Repository | |
| Languages | JavaScript |
Overview
-
Implement custom event groups in the JavaScript Scheduler component using DayPilot Lite. Automatic concurrent event groups are a separate Pro feature.
-
Add an active area to event groups that allows expanding the group using a [+] icon
-
Add an active area to expanded event members that allows collapsing the group using a [-] icon
-
See also how to highlight related Scheduler events on hover and force a specific position of related events
License
The sample uses DayPilot Lite for JavaScript, available under the Apache License 2.0. The tutorial source code is also licensed under Apache License 2.0. Automatic concurrent event grouping requires DayPilot Pro for JavaScript and its commercial license.
Automatic Grouping of Scheduler Events
DayPilot Pro for JavaScript supports automatic grouping of concurrent events. This feature helps with compacting the view if there are many events. The groups can be expanded on click.
The grouping is fully automatic and all events that are part of an overlapping block of events will be included.
This automatic grouping feature is available in DayPilot Pro. The custom groups described below work in DayPilot Lite: your application chooses the members, regardless of whether their dates overlap.
Running the Sample
Install dependencies with npm ci and start the development server with npm run dev. Open the local URL printed by Vite. Use npm run build to create a production build.
The src/app.js module imports DayPilot from @daypilot/daypilot-lite-javascript. The page loads this module with a module script. You can also create a Scheduler starter using the DayPilot UI Builder.
Defining a Group of Related Events
Let's say we have two events that we want to display as a single group. Each member uses a custom group property containing the summary event ID:
members: [
{
id: 1,
start: "2026-11-02T00:00:00",
end: "2026-11-06T00:00:00",
text: "Event 1 (Group 1)",
group: 101,
resource: "R2"
},
{
id: 2,
start: "2026-11-05T00:00:00",
end: "2026-11-10T00:00:00",
text: "Event 2 (Group 1)",
group: 101,
resource: "R2"
}
]
In order to display them as one event, we need to replace these two records with a single one that defines the group. We'll also add properties that define the boundaries (start, end), name (text) and a custom CSS class (cssClass) to distinguish the group from its members. The summary spans the earliest start and latest end of its members; the duration bar is hidden in this example.
The group members will be stored in a members property. This is a custom property name and you can change it as you wish (just make sure it doesn't conflict with the predefined properties).
const events = [
{
id: 101,
start: "2026-11-02T00:00:00",
end: "2026-11-10T00:00:00",
text: "Group 1",
resource: "R2",
cssClass: "event-group",
expanded: false,
members: [
// ...
]
},
// ...
];
Adding Expand Icon [+] to Groups
In order to add an expand icon to the groups, we will use onBeforeEventRender event to add an active area to the event.
First, we need to check if the event is a group (it has args.data.members property). Then we add an active area (positioned using right, top, width and height properties) with an SVG icon (symbol property).
The onClick event handler replaces the summary with its members. It first stores the summary record in app.groups, a JavaScript Map keyed by group ID. Then it removes the summary from the visible events array, adds its members, and calls dp.update({events}) once. Unrelated events stay in the array. This uses Lite APIs without relying on an event hidden flag.
const dp = new DayPilot.Scheduler("dp", {
// ...
onBeforeEventRender: (args) => {
const areas = [];
// This is a group event: add an expand icon.
if (args.data.members) {
areas.push({
right: 6,
top: 9,
height: 17,
width: 17,
fontColor: "#777",
symbol: "icons/daypilot.svg#plus-4",
cssClass: "expand-group",
cursor: "pointer",
toolTip: "Expand group",
onClick: (args) => {
const group = args.source.data;
group.expanded = true;
app.groups.set(group.id, group);
const events = dp.events.list.filter(data => data.id !== group.id);
events.push(...group.members);
dp.update({events});
}
});
}
// ...
args.data.areas = areas;
}
});
Adding Collapse Icon [-] to Event Group Members
We will use the same logic to add the collapse icon to the group members.
First, we detect a group member by checking args.data.group property. We add an active area at the same position but this time we use a different SVG icon (daypilot.svg#minus-4).
The onClick handler looks up the saved summary in app.groups and collects the current member records from dp.events.list. It saves those records back to group.members before replacing them with the summary. This preserves member changes across repeated collapse/expand cycles. The handler also recalculates the summary start and end from the current members.
The custom expanded property is set to true on expansion and false on collapse. The map and event list keep this state in memory; reloading the page restores the original demonstration data. Move or resize individual members while expanded. The sample prevents moving or resizing the collapsed summary, because doing so would not update the member dates.
const dp = new DayPilot.Scheduler("dp", {
// ...
onBeforeEventRender: (args) => {
const areas = [];
// ...
else if (args.data.group) {
areas.push({
right: 6,
top: 9,
height: 17,
width: 17,
fontColor: "#777",
symbol: "icons/daypilot.svg#minus-4",
cssClass: "collapse-group",
cursor: "pointer",
toolTip: "Collapse group",
onClick: (args) => {
const group = app.groups.get(args.source.data.group);
group.members = dp.events.list.filter(data => data.group === group.id);
group.start = group.members.reduce((start, data) =>
new DayPilot.Date(data.start) < new DayPilot.Date(start) ? data.start : start,
group.members[0].start);
group.end = group.members.reduce((end, data) =>
new DayPilot.Date(data.end) > new DayPilot.Date(end) ? data.end : end,
group.members[0].end);
group.expanded = false;
const events = dp.events.list.filter(data => data.group !== group.id);
events.push(group);
dp.update({events});
}
});
}
args.data.areas = areas;
}
});
Full Source Code
The page shell is index.html. It loads the module and defines the green member and blue summary styling:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JavaScript Scheduler: Expandable Event Groups</title>
<style>
p, body, td, input, select, button { font-family: -apple-system,system-ui,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif; font-size: 14px; }
body { padding: 0px; margin: 0px; background-color: #ffffff; }
a { color: #1155a3; }
.space { margin: 10px 0px 10px 0px; }
.header { background: #003267; background: linear-gradient(to right, #011329 0%,#00639e 44%,#011329 100%); padding:20px 10px; color: white; box-shadow: 0px 0px 10px 5px rgba(0,0,0,0.75); }
.header a { color: white; }
.header h1 a { text-decoration: none; }
.header h1 { padding: 0px; margin: 0px; }
.main { padding: 10px; margin-top: 10px; }
#dp .scheduler_default_event_inner { background: #ddf8dd; border: 1px solid #93fd93; border-radius: 8px; padding: 4px 28px 4px 4px; }
#dp .event-group .scheduler_default_event_inner { background: #e6f7ff; border: 1px solid #b3e0ff; }
.generated { color: #999; }
.generated a { color: #999; }
</style>
</head>
<body>
<div class="header">
<h1><a href='https://code.daypilot.org/22806/javascript-scheduler-expandable-event-groups'>JavaScript Scheduler: Expandable Event Groups</a></h1>
<div><a href="https://javascript.daypilot.org/">DayPilot for JavaScript</a> - HTML5 Calendar/Scheduling Components for JavaScript/Angular/React/Vue</div>
</div>
<div class="main">
<div id="dp"></div>
<div class="generated">Generated using <a href="https://builder.daypilot.org/">DayPilot UI Builder</a>.</div>
</div>
<script type="module" src="/src/app.js"></script>
</body>
</html>
The complete grouping implementation is in src/app.js. Select an empty time range to create an independent event; it is not automatically assigned to a group.
import {DayPilot} from "@daypilot/daypilot-lite-javascript";
const dp = new DayPilot.Scheduler("dp", {
timeHeaders: [{groupBy: "Month"}, {groupBy: "Day", format: "d"}],
scale: "Day",
days: 30,
startDate: "2026-11-01",
timeRangeSelectedHandling: "Enabled",
durationBarVisible: false,
eventHeight: 35,
rowHeaderWidth: 100,
onTimeRangeSelected: async (args) => {
const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
dp.clearSelection();
if (modal.canceled) {
return;
}
dp.events.add({
start: args.start,
end: args.end,
id: DayPilot.guid(),
resource: args.resource,
text: modal.result
});
},
onEventMove: (args) => {
if (args.e.data.members) {
args.preventDefault();
}
},
onEventResize: (args) => {
if (args.e.data.members) {
args.preventDefault();
}
},
onBeforeEventRender: (args) => {
const areas = [];
// This is a group event: add an expand icon.
if (args.data.members) {
areas.push({
right: 6,
top: 9,
height: 17,
width: 17,
fontColor: "#777",
symbol: "icons/daypilot.svg#plus-4",
cssClass: "expand-group",
cursor: "pointer",
toolTip: "Expand group",
onClick: (args) => {
const group = args.source.data;
group.expanded = true;
app.groups.set(group.id, group);
const events = dp.events.list.filter(data => data.id !== group.id);
events.push(...group.members);
dp.update({events});
}
});
}
// This is a member event: add a collapse icon.
else if (args.data.group) {
areas.push({
right: 6,
top: 9,
height: 17,
width: 17,
fontColor: "#777",
symbol: "icons/daypilot.svg#minus-4",
cssClass: "collapse-group",
cursor: "pointer",
toolTip: "Collapse group",
onClick: (args) => {
const group = app.groups.get(args.source.data.group);
group.members = dp.events.list.filter(data => data.group === group.id);
group.start = group.members.reduce((start, data) =>
new DayPilot.Date(data.start) < new DayPilot.Date(start) ? data.start : start,
group.members[0].start);
group.end = group.members.reduce((end, data) =>
new DayPilot.Date(data.end) > new DayPilot.Date(end) ? data.end : end,
group.members[0].end);
group.expanded = false;
const events = dp.events.list.filter(data => data.group !== group.id);
events.push(group);
dp.update({events});
}
});
}
args.data.areas = areas;
}
});
dp.init();
const app = {
groups: new Map(),
init() {
const resources = [
{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"}
];
const events = [
{
id: 101,
start: "2026-11-02T00:00:00",
end: "2026-11-10T00:00:00",
text: "Group 1",
resource: "R2",
cssClass: "event-group",
expanded: false,
members: [
{
id: 1,
start: "2026-11-02T00:00:00",
end: "2026-11-06T00:00:00",
text: "Event 1 (Group 1)",
group: 101,
resource: "R2"
},
{
id: 2,
start: "2026-11-05T00:00:00",
end: "2026-11-10T00:00:00",
text: "Event 2 (Group 1)",
group: 101,
resource: "R2"
}
]
},
{
id: 3,
start: "2026-11-03T00:00:00",
end: "2026-11-08T00:00:00",
text: "Event 3",
resource: "R4"
}
];
dp.update({resources, events});
}
};
app.init();
DayPilot




