Features
Implementation of custom 5-minute snap-to-grid rules using
onTimeRangeSelecting,onTimeRangeSelected,onEventMoving, andonEventResizing.The snap-to-grid settings do not have to correspond to the Scheduler grid cells. This example uses one cell per hour while the interactions snap to 5-minute steps.
The downloadable sample uses the current JavaScript DayPilot UI Builder template and keeps the feature-specific logic in a single
index.htmlfile.
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.
JavaScript Scheduler Configuration
Let’s start with the current JavaScript Scheduler setup. The page shell and asset layout come from the DayPilot UI Builder template, but the custom snap-to-grid logic stays in index.html, split between a small app helper object and the Scheduler configuration:
const app = {
snapToMillis: 5 * 60 * 1000,
snapDown(date) {
const offset = date.getTimePart() % this.snapToMillis;
return offset ? date.addMilliseconds(-offset) : date;
},
snapUp(date) {
const offset = date.getTimePart() % this.snapToMillis;
return offset ? date.addMilliseconds(-offset).addMilliseconds(this.snapToMillis) : date;
},
updateRangeIndicators(args) {
args.left.enabled = true;
args.left.html = args.start.toString("h:mm tt");
args.right.enabled = true;
args.right.html = args.end.toString("h:mm tt");
},
init() {
const resources = Array.from({length: 7}, (_, index) => ({
name: `Resource ${index + 1}`,
id: `R${index + 1}`
}));
const events = [];
dp.update({resources, events});
}
};
const dp = new DayPilot.Scheduler("dp", {
timeHeaders: [
{groupBy: "Day"},
{groupBy: "Hour"}
],
scale: "Hour",
days: 7,
startDate: DayPilot.Date.today().firstDayOfWeek(),
cellWidth: 60,
snapToGrid: false,
snapToGridEventMoving: false,
snapToGridEventResizing: false,
snapToGridTimeRangeSelecting: false,
useEventBoxes: "Never",
// ...
});
dp.init();
app.init();This configuration displays the current week with one cell per hour:
scale: "Hour",
days: 7,
startDate: DayPilot.Date.today().firstDayOfWeek(),
cellWidth: 60,The time headers display the date in the first row and the hours in the second row:
timeHeaders: [
{groupBy: "Day"},
{groupBy: "Hour"}
]The built-in snap-to-grid behavior is turned off for selection, moving, and resizing because we will handle each interaction ourselves:
snapToGrid: false,
snapToGridEventMoving: false,
snapToGridEventResizing: false,
snapToGridTimeRangeSelecting: false,The event boxes are turned off as well so the events can keep the exact snapped start and end values instead of stretching to full hour cells:
useEventBoxes: "Never"Snap-to-Grid for Event Creation

New events can be created using drag-and-drop time range selecting. We use the real-time onTimeRangeSelecting handler to snap the selection as the user drags and to display the adjusted start and end labels.
onTimeRangeSelecting: (args) => {
args.start = app.snapDown(args.start);
args.end = app.snapUp(args.end);
app.updateRangeIndicators(args);
},When the mouse button is released, onTimeRangeSelected fires. The sample opens a prompt and adds the event using the already-snapped args.start and args.end values:
onTimeRangeSelected: async (args) => {
const scheduler = args.control;
const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
scheduler.clearSelection();
if (modal.canceled) {
return;
}
scheduler.events.add({
start: args.start,
end: args.end,
id: DayPilot.guid(),
resource: args.resource,
text: modal.result
});
},This keeps the creation workflow simple: the selection preview shows the final 5-minute-aligned range, and the same snapped range is stored when the event is created.
Snap-to-Grid for Event Moving

The next step is customizing drag-and-drop event moving. The onEventMoving handler snaps the start time down to the nearest 5-minute boundary, shifts the end time by the same offset, and updates the preview labels:
onEventMoving: (args) => {
const offset = args.start.getTimePart() % app.snapToMillis;
if (offset) {
args.start = args.start.addMilliseconds(-offset);
args.end = args.end.addMilliseconds(-offset);
}
app.updateRangeIndicators(args);
},Because the same offset is applied to both dates, the event duration stays unchanged while the whole item snaps to the 5-minute grid.
Snap-to-Grid for Event Resizing

We also need to adjust event resizing. In onEventResizing, DayPilot exposes the fixed edge of the resize shadow as args.anchor. Using that fixed edge is a more universal approach than relying on args.what, and it uses the same anchor-based approach as onEventMoving:
onEventResizing: (args) => {
if (args.anchor.equals(args.start)) {
args.end = app.snapUp(args.end);
}
else {
args.start = app.snapDown(args.start);
}
app.updateRangeIndicators(args);
}This keeps the non-dragged edge fixed during event resizing. The start edge uses snapDown() and the end edge uses snapUp(), which preserves the same behavior as the selection preview: the visible range always expands to the next valid 5-minute endpoint instead of ending between snap positions.
Full Source Code
Here is the full source code of the updated plain JavaScript sample:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JavaScript Scheduler: Customized Snap-to-Grid</title>
<style type="text/css">
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; }
.generated { color: #999; }
.generated a { color: #999; }
</style>
<script src="js/daypilot/daypilot-all.min.js"></script>
</head>
<body>
<div class="header">
<h1><a href="https://code.daypilot.org/39403/javascript-scheduler-customized-snap-to-grid">JavaScript Scheduler: Customized Snap-to-Grid</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>
const app = {
snapToMinutes: 5,
snapToMillis: 5 * 60 * 1000,
snapDown(date) {
const offset = date.getTimePart() % this.snapToMillis;
return offset ? date.addMilliseconds(-offset) : date;
},
snapUp(date) {
const offset = date.getTimePart() % this.snapToMillis;
return offset ? date.addMilliseconds(-offset).addMilliseconds(this.snapToMillis) : date;
},
updateRangeIndicators(args) {
args.left.enabled = true;
args.left.html = args.start.toString("h:mm tt");
args.right.enabled = true;
args.right.html = args.end.toString("h:mm tt");
},
init() {
const resources = Array.from({length: 7}, (_, index) => ({
name: `Resource ${index + 1}`,
id: `R${index + 1}`
}));
const events = [];
dp.update({resources, events});
}
};
const dp = new DayPilot.Scheduler("dp", {
timeHeaders: [
{groupBy: "Day"},
{groupBy: "Hour"}
],
scale: "Hour",
days: 7,
startDate: DayPilot.Date.today().firstDayOfWeek(),
cellWidth: 60,
snapToGrid: false,
snapToGridEventMoving: false,
snapToGridEventResizing: false,
snapToGridTimeRangeSelecting: false,
useEventBoxes: "Never",
timeRangeSelectedHandling: "Enabled",
eventMoveHandling: "Update",
eventResizeHandling: "Update",
eventHoverHandling: "Bubble",
bubble: new DayPilot.Bubble({
onLoad: (args) => {
const event = args.source;
const start = event.start().toString("M/d/yyyy h:mm tt");
const end = event.end().toString("M/d/yyyy h:mm tt");
args.html = `${start}<br>${end}`;
}
}),
onTimeRangeSelecting: (args) => {
args.start = app.snapDown(args.start);
args.end = app.snapUp(args.end);
app.updateRangeIndicators(args);
},
onTimeRangeSelected: async (args) => {
const scheduler = args.control;
const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
scheduler.clearSelection();
if (modal.canceled) {
return;
}
scheduler.events.add({
start: args.start,
end: args.end,
id: DayPilot.guid(),
resource: args.resource,
text: modal.result
});
},
onEventMoving: (args) => {
const offset = args.start.getTimePart() % app.snapToMillis;
if (offset) {
args.start = args.start.addMilliseconds(-offset);
args.end = args.end.addMilliseconds(-offset);
}
app.updateRangeIndicators(args);
},
onEventResizing: (args) => {
if (args.anchor.equals(args.start)) {
args.end = app.snapUp(args.end);
}
else {
args.start = app.snapDown(args.start);
}
app.updateRangeIndicators(args);
}
});
dp.init();
app.init();
</script>
</body>
</html>History
2026-04-21: Upgraded the bundled DayPilot Pro for JavaScript runtime to 2026.2.6907, refreshed the screenshots, and reviewed the article text.
DayPilot




