Skip to content

Drag & Drop

trud-calendar uses the Pointer Events API for all drag interactions — working seamlessly on mouse, touch screens, and stylus devices with zero extra dependencies.

Set enableDnD={true} and provide handlers:

<Calendar
events={events}
enableDnD
onEventDrop={(event, newStart, newEnd) => {
await api.updateEvent(event.id, { start: newStart, end: newEnd });
}}
onEventResize={(event, newStart, newEnd) => {
await api.updateEvent(event.id, { start: newStart, end: newEnd });
}}
onSlotSelect={(start, end) => {
// User dragged across empty slots to select a time range
openCreateModal({ start, end });
}}
/>
  • Month view: Drag an event pill from one day cell to another. The date changes but the original time is preserved.
  • Week/Day view: Drag an event from one time slot to another. Both date and time change, snapping to the configured increment.
  • Event duration is always preserved.
  • In Week/Day view, drag the top edge of an event to change its start time, or the bottom edge to change its end time.
  • Snaps to the configured increment (default 15 minutes).
  • Fires onEventResize with the updated start/end times.
  • Drag across empty time slots in Week/Day view to select a time range.
  • Fires onSlotSelect(start, end) with the selected range.
  • Great for “create event” flows — the user drags to select when, then fills in the details.

Use display: "background" on events to render them as colored time blocks behind regular events. Great for showing business hours, availability, or blocked time:

const events = [
{
id: "business-hours",
title: "Business Hours",
start: "2026-03-25T08:00:00",
end: "2026-03-25T18:00:00",
display: "background",
color: "#22c55e",
},
// ... regular events
];
<Calendar events={events} />

Background events are non-interactive — they cannot be dragged, resized, or clicked.

When dragging, resizing, or selecting near the top or bottom edge of the time grid, the container automatically scrolls to reveal more content. No configuration needed — it activates during any pointer interaction.

All interactions work on touch devices out of the box:

  • Touch and hold an event, then drag to move
  • Touch the resize handle at the bottom of an event and drag to resize
  • Touch and drag across empty slots to select a time range

The calendar uses touch-action: none on interactive elements to prevent the browser from scrolling during drag operations. A 5px movement threshold distinguishes taps from drags.

On touch devices, you may want to require a long press before drag starts, to avoid interfering with scrolling:

<Calendar
events={events}
enableDnD
longPressDelay={300} // 300ms hold before drag activates on touch
onEventDrop={handleDrop}
/>

This only affects touch interactions — mouse and stylus dragging remains immediate.

onEventDrop?: (
event: CalendarEvent, // The original event object
newStart: DateTimeString, // "2026-03-15T10:00:00"
newEnd: DateTimeString, // "2026-03-15T11:00:00"
) => void;
onEventResize?: (
event: CalendarEvent, // The original event object
newStart: DateTimeString, // "2026-03-15T10:00:00"
newEnd: DateTimeString, // "2026-03-15T11:30:00" (new duration)
) => void;
onSlotSelect?: (
start: DateTimeString, // "2026-03-15T10:00:00"
end: DateTimeString, // "2026-03-15T11:00:00"
) => void;

By default, all interactions snap to 15-minute increments. Change it with snapDuration:

<Calendar
events={events}
enableDnD
snapDuration={30} // Snap to 30-minute increments
onEventDrop={handleDrop}
onEventResize={handleResize}
/>

Common values: 5, 10, 15, 30, 60.

Control where events can be dropped, resized, or selected by providing constraint callbacks. Return false to prevent the action:

<Calendar
events={events}
enableDnD
// Only allow drops within business hours
dragConstraint={(event, newStart, newEnd) => {
const hour = new Date(newStart).getHours();
return hour >= 8 && hour < 18;
}}
// Prevent resizing past 2 hours
resizeConstraint={(event, newStart, newEnd) => {
const ms = new Date(newEnd).getTime() - new Date(newStart).getTime();
return ms <= 2 * 60 * 60 * 1000;
}}
// Only allow selecting within business hours
selectConstraint={(start, end) => {
const hour = new Date(start).getHours();
return hour >= 8 && hour < 18;
}}
onEventDrop={handleDrop}
onEventResize={handleResize}
onSlotSelect={handleSelect}
/>

When a constraint returns false, the interaction is silently reverted — the event returns to its original position and no callback fires.

function App() {
const [events, setEvents] = useState(initialEvents);
const [creating, setCreating] = useState(null);
const handleDrop = async (event, newStart, newEnd) => {
// Optimistic update
setEvents((prev) =>
prev.map((e) =>
e.id === event.id ? { ...e, start: newStart, end: newEnd } : e
)
);
await fetch(`/api/events/${event.id}`, {
method: "PATCH",
body: JSON.stringify({ start: newStart, end: newEnd }),
});
};
return (
<Calendar
events={events}
enableDnD
onEventDrop={handleDrop}
onEventResize={handleDrop}
onSlotSelect={(start, end) => setCreating({ start, end })}
/>
);
}