ResourcesMonthView
Month view with resource lanes for scheduling across resources
Source
LLM docs
Docs
Package
Usage
ResourcesMonthView displays a month grid where each day cell contains resource lanes. Events are shown within their resource's lane, making it easy to see resource utilization across the month.
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView } from '@mantine/schedule';
import { events, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}Static mode
Set mode="static" to disable all interactions.
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView } from '@mantine/schedule';
import { events, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
mode="static"
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}Drag and drop
Enable drag and drop with withEventsDragAndDrop prop. The onEventDrop callback receives
the resourceId of the target resource, use it to update the event's resource.
To reject a drop based on where the event lands, for example to prevent double booking, use
preventEventOverlap and canDropEvent, see drop validation.
In resource views, the event is checked against the events of the target resource. Month view has no drag ghost,
a rejected drop is only reported with onEventPlacementRejected.
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, ScheduleEventData } from '@mantine/schedule';
import { events as initialEvents, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
const [events, setEvents] = useState<ScheduleEventData[]>(initialEvents);
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
withEventsDragAndDrop
onEventDrop={({ eventId, newStart, newEnd, resourceId }) => {
setEvents((current) =>
current.map((event) =>
event.id === eventId
? { ...event, start: newStart, end: newEnd, resourceId }
: event
)
);
}}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}Event resize
Enable event resizing with withEventResize prop. Events can be resized by dragging their start or
end edges, onEventResize is called with the new start and end dates.
To reject a resize based on the new range, use preventEventOverlap and canResizeEventTo, see
drop validation.
- Resizing snaps to whole days and keeps the time of day: an event that ends at
17:00still ends at17:00on the new day - Resize handles are displayed only on the edges where the event starts and ends, an event that continues outside the displayed month has no handle on that edge
- Events hidden behind the "+N more" indicator cannot be resized
- Resizing does not change the event's resource, drag the event to move it to another resource
- Use
canResizeEventto control which events can be resized
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, ScheduleEventData } from '@mantine/schedule';
import { events as initialEvents, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
const [events, setEvents] = useState<ScheduleEventData[]>(initialEvents);
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
withEventResize
onEventResize={({ eventId, newStart, newEnd }) => {
setEvents((current) =>
current.map((event) =>
event.id === eventId
? { ...event, start: newStart, end: newEnd }
: event
)
);
}}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}Event form
Click a day cell to create a new event, or click an existing event to edit it. Use withDragSlotSelect
and onSlotDragEnd to allow drag-to-create across day cells. The onSlotDragEnd callback includes
the resourceId of the resource where the drag started.
import dayjs from 'dayjs';
import { useState } from 'react';
import { Select } from '@mantine/core';
import { ResourcesMonthView, ScheduleEventData } from '@mantine/schedule';
import { EventData, EventForm } from './EventForm';
import { events as initialEvents, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
const [events, setEvents] = useState<ScheduleEventData[]>(initialEvents);
const [formOpened, setFormOpened] = useState(false);
const [selectedEventData, setSelectedEventData] = useState<EventData | null>(null);
const [selectedResourceId, setSelectedResourceId] = useState<string | null>(null);
const handleDayClick = ({
date: clickedDate,
resourceId,
}: {
date: string;
nativeEvent: React.MouseEvent<HTMLButtonElement>;
resourceId?: string | number;
}) => {
setSelectedResourceId(resourceId ? String(resourceId) : null);
setSelectedEventData({
title: '',
start: dayjs(clickedDate).startOf('day').toDate(),
end: dayjs(clickedDate).endOf('day').toDate(),
color: 'blue',
});
setFormOpened(true);
};
const handleEventClick = (event: ScheduleEventData) => {
setSelectedResourceId(event.resourceId ? String(event.resourceId) : null);
setSelectedEventData({
id: event.id,
title: event.title,
start: new Date(event.start),
end: new Date(event.end),
color: event.color || 'blue',
});
setFormOpened(true);
};
const handleSlotDragEnd = ({
rangeStart,
rangeEnd,
resourceId,
}: {
rangeStart: string;
rangeEnd: string;
resourceId?: string | number;
}) => {
setSelectedResourceId(resourceId ? String(resourceId) : null);
setSelectedEventData({
title: '',
start: new Date(rangeStart),
end: new Date(rangeEnd),
color: 'blue',
});
setFormOpened(true);
};
const handleSubmit = (values: EventData) => {
if (values.id) {
setEvents((prev) =>
prev.map((event) =>
event.id === values.id
? {
...event,
title: values.title,
start: dayjs(values.start).toISOString(),
end: dayjs(values.end).toISOString(),
color: values.color || 'blue',
resourceId: selectedResourceId || event.resourceId,
}
: event
)
);
} else {
setEvents((prev) => [
...prev,
{
id: Math.random().toString(36).substring(2, 11),
title: values.title,
start: dayjs(values.start).toISOString(),
end: dayjs(values.end).toISOString(),
color: values.color || 'blue',
resourceId: selectedResourceId || undefined,
},
]);
}
};
const handleDeleteEvent = () => {
if (selectedEventData?.id) {
setEvents((prev) => prev.filter((e) => e.id !== selectedEventData.id));
}
};
return (
<>
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
withDragSlotSelect
onDayClick={handleDayClick}
onSlotDragEnd={handleSlotDragEnd}
onEventClick={handleEventClick}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
<EventForm
opened={formOpened}
onClose={() => setFormOpened(false)}
onExitTransitionEnd={() => setSelectedEventData(null)}
values={selectedEventData}
onSubmit={handleSubmit}
onDelete={selectedEventData?.id ? handleDeleteEvent : undefined}
>
<Select
label="Resource"
placeholder="Select resource"
radius="md"
data={resources.map((r) => ({ value: String(r.id), label: r.label }))}
value={selectedResourceId}
onChange={setSelectedResourceId}
/>
</EventForm>
</>
);
}Custom event rendering
Use renderEvent prop to customize how events are rendered. The example below uses HoverCard
to display event details on hover.
import dayjs from 'dayjs';
import { useState } from 'react';
import { HoverCard, UnstyledButton } from '@mantine/core';
import { ResourcesMonthView, ScheduleEventData } from '@mantine/schedule';
import { EventDetails } from './EventDetails';
import { events as initialEvents, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
const [events, setEvents] = useState<ScheduleEventData[]>(initialEvents);
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
withEventsDragAndDrop
onEventDrop={({ eventId, newStart, newEnd, resourceId }) => {
setEvents((current) =>
current.map((event) =>
event.id === eventId
? { ...event, start: newStart, end: newEnd, resourceId }
: event
)
);
}}
renderEvent={(event, props) => (
<HoverCard width={280} position="right" closeDelay={0} transitionProps={{ duration: 0 }}>
<HoverCard.Target>
<UnstyledButton {...props} />
</HoverCard.Target>
<HoverCard.Dropdown>
<EventDetails event={event} resources={resources} />
</HoverCard.Dropdown>
</HoverCard>
)}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}Custom resource label
Use renderResourceLabel prop to customize how resource labels are rendered in the row headers.
Meeting room: Tokyo
Office
Meeting room: Paris
Office
Meeting room: New York
Office
import dayjs from 'dayjs';
import { useState } from 'react';
import { Stack, Text } from '@mantine/core';
import { ResourcesMonthView, ScheduleResourceData } from '@mantine/schedule';
import { events, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
renderResourceLabel={(resource: ScheduleResourceData) => (
<Stack gap={0} align="flex-start">
<Text fw={600} size="sm">
{resource.label}
</Text>
<Text size="xs" c="dimmed">
Office
</Text>
</Stack>
)}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}Resource groups
Use groups prop to group resources under labeled headers. The group labels are displayed
as a column to the left of resource labels, spanning vertically across their resources.
Use renderGroupLabel to customize group label rendering and groupLabelWidth to control
the group column width.
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, ScheduleResourceGroup } from '@mantine/schedule';
import { events } from './data';
const resources = [
{ id: 'tokyo', label: 'Meeting room: Tokyo' },
{ id: 'paris', label: 'Meeting room: Paris' },
{ id: 'new-york', label: 'Meeting room: New York' },
{ id: 'overflow', label: 'Overflow room' },
];
const groups: ScheduleResourceGroup[] = [
{ label: 'Floor 1', resourceIds: ['tokyo', 'paris'] },
{ label: 'Floor 2', resourceIds: ['new-york'] },
];
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
groups={groups}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}Localization
Set locale prop to change the language. Use labels prop to override built-in labels.
import 'dayjs/locale/es';
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView } from '@mantine/schedule';
import { events, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
locale="es"
labels={{
day: 'DÃa',
week: 'Semana',
month: 'Mes',
year: 'Año',
today: 'Hoy',
previous: 'Anterior',
next: 'Siguiente',
more: 'Más',
resources: 'Recursos',
moreLabel: (hiddenEventsCount) => `+${hiddenEventsCount} más`,
}}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}Recurring events
ResourcesMonthView automatically expands recurring events for the visible month. See Recurring events guide for full documentation.
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const resources = [
{ id: 'tokyo', label: 'Meeting room: Tokyo' },
{ id: 'paris', label: 'Meeting room: Paris' },
];
const events = [
{
id: 'daily-sync-series',
title: 'Daily sync (series)',
start: `${dayjs(today).subtract(2, 'day').format('YYYY-MM-DD')} 09:00:00`,
end: `${dayjs(today).subtract(2, 'day').format('YYYY-MM-DD')} 09:30:00`,
color: 'blue',
resourceId: 'tokyo',
recurrence: {
rrule: 'FREQ=DAILY;COUNT=10',
exdate: [`${today} 09:00:00`],
},
},
{
id: 'daily-sync-override',
title: 'Daily sync (moved today)',
start: `${today} 14:00:00`,
end: `${today} 14:30:00`,
color: 'grape',
resourceId: 'tokyo',
recurringEventId: 'daily-sync-series',
recurrenceId: `${today} 09:00:00`,
},
{
id: 'one-off',
title: 'One-off planning',
start: `${today} 11:00:00`,
end: `${today} 12:00:00`,
color: 'green',
resourceId: 'paris',
},
];
function Demo() {
const [date, setDate] = useState(today);
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
startScrollDate={today}
/>
);
}Max events per cell
Use maxEventsPerTimeSlot prop to limit the number of events visible in each cell.
When there are more events than the limit, a "+more" indicator is shown.
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, ScheduleEventData, ScheduleResourceData } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const resources: ScheduleResourceData[] = [
{ id: 'tokyo', label: 'Meeting room: Tokyo' },
{ id: 'paris', label: 'Meeting room: Paris' },
];
const events: ScheduleEventData[] = [
{ id: 1, title: 'Team Meeting', start: `${today} 09:00:00`, end: `${today} 10:00:00`, color: 'blue', resourceId: 'tokyo' },
{ id: 2, title: 'Code Review', start: `${today} 11:00:00`, end: `${today} 12:00:00`, color: 'green', resourceId: 'tokyo' },
{ id: 3, title: 'Design Review', start: `${today} 13:00:00`, end: `${today} 14:00:00`, color: 'violet', resourceId: 'tokyo' },
{ id: 4, title: 'Sprint Planning', start: `${today} 15:00:00`, end: `${today} 16:00:00`, color: 'orange', resourceId: 'tokyo' },
{ id: 5, title: 'Retrospective', start: `${today} 16:30:00`, end: `${today} 17:30:00`, color: 'cyan', resourceId: 'tokyo' },
{ id: 6, title: 'Client Call', start: `${today} 09:00:00`, end: `${today} 10:00:00`, color: 'pink', resourceId: 'paris' },
{ id: 7, title: 'Workshop', start: `${today} 11:00:00`, end: `${today} 12:00:00`, color: 'grape', resourceId: 'paris' },
{ id: 8, title: 'Standup', start: `${today} 14:00:00`, end: `${today} 15:00:00`, color: 'red', resourceId: 'paris' },
{ id: 9, title: 'Demo', start: `${today} 16:00:00`, end: `${today} 17:00:00`, color: 'teal', resourceId: 'paris' },
];
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
maxEventsPerTimeSlot={3}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}Day width and row height
Use dayWidth and rowHeight props to customize the dimensions of day columns and resource rows.
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView } from '@mantine/schedule';
import { events, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
dayWidth={60}
rowHeight={64}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}Start scroll date
Use startScrollDate prop to scroll to a specific date on initial render, for example to today.
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView } from '@mantine/schedule';
import { events, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}Without weekend days
Set withWeekendDays={false} to hide weekend day columns from the month grid.
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView } from '@mantine/schedule';
import { events, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
withWeekendDays={false}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}More events indicator
Click the "+X more" indicator to see all events of the day.
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, ScheduleEventData, ScheduleResourceData } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const resources: ScheduleResourceData[] = [
{ id: 'tokyo', label: 'Meeting room: Tokyo' },
{ id: 'paris', label: 'Meeting room: Paris' },
];
const events: ScheduleEventData[] = [
{ id: 1, title: 'Team Standup', start: `${today} 08:00:00`, end: `${today} 08:30:00`, color: 'blue', resourceId: 'tokyo' },
{ id: 2, title: 'Sprint Planning', start: `${today} 09:00:00`, end: `${today} 10:00:00`, color: 'green', resourceId: 'tokyo' },
{ id: 3, title: 'Design Review', start: `${today} 10:30:00`, end: `${today} 11:30:00`, color: 'violet', resourceId: 'tokyo' },
{ id: 4, title: 'Code Review', start: `${today} 13:00:00`, end: `${today} 14:00:00`, color: 'orange', resourceId: 'tokyo' },
{ id: 5, title: 'Retrospective', start: `${today} 14:30:00`, end: `${today} 15:30:00`, color: 'cyan', resourceId: 'tokyo' },
{ id: 6, title: 'Client Call', start: `${today} 16:00:00`, end: `${today} 17:00:00`, color: 'pink', resourceId: 'tokyo' },
{ id: 7, title: 'Workshop', start: `${today} 10:00:00`, end: `${today} 11:00:00`, color: 'grape', resourceId: 'paris' },
];
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
maxEventsPerTimeSlot={2}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}More events props
Use moreEventsProps to customize the more events dropdown, for example to use a modal
instead of a popover.
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, ScheduleEventData, ScheduleResourceData } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const resources: ScheduleResourceData[] = [
{ id: 'tokyo', label: 'Meeting room: Tokyo' },
{ id: 'paris', label: 'Meeting room: Paris' },
];
const events: ScheduleEventData[] = [
{ id: 1, title: 'Team Standup', start: `${today} 08:00:00`, end: `${today} 08:30:00`, color: 'blue', resourceId: 'tokyo' },
{ id: 2, title: 'Sprint Planning', start: `${today} 09:00:00`, end: `${today} 10:00:00`, color: 'green', resourceId: 'tokyo' },
{ id: 3, title: 'Design Review', start: `${today} 10:30:00`, end: `${today} 11:30:00`, color: 'violet', resourceId: 'tokyo' },
{ id: 4, title: 'Code Review', start: `${today} 13:00:00`, end: `${today} 14:00:00`, color: 'orange', resourceId: 'tokyo' },
{ id: 5, title: 'Retrospective', start: `${today} 14:30:00`, end: `${today} 15:30:00`, color: 'cyan', resourceId: 'tokyo' },
{ id: 6, title: 'Client Call', start: `${today} 16:00:00`, end: `${today} 17:00:00`, color: 'pink', resourceId: 'tokyo' },
{ id: 7, title: 'Workshop', start: `${today} 10:00:00`, end: `${today} 11:00:00`, color: 'grape', resourceId: 'paris' },
];
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
maxEventsPerTimeSlot={2}
startScrollDate={dayjs().format('YYYY-MM-DD')}
moreEventsProps={{ dropdownType: 'modal', modalTitle: 'All Events' }}
/>
);
}Custom header
Set withHeader={false} to hide the default header and use ScheduleHeader to build
a custom header with navigation controls.
import dayjs from 'dayjs';
import { useState } from 'react';
import { DateStringValue, ResourcesMonthView, ScheduleHeader } from '@mantine/schedule';
import { events, resources } from './data';
function Demo() {
const [date, setDate] = useState<DateStringValue>(
dayjs().format('YYYY-MM-DD')
);
return (
<div>
<ScheduleHeader>
<ScheduleHeader.Previous
onClick={() =>
setDate(
dayjs(date)
.subtract(1, 'month')
.startOf('month')
.format('YYYY-MM-DD') as DateStringValue
)
}
/>
<ScheduleHeader.MonthYearSelect
yearValue={dayjs(date).year()}
monthValue={dayjs(date).month()}
onYearChange={(year) =>
setDate(
dayjs(date)
.year(year)
.startOf('month')
.format('YYYY-MM-DD') as DateStringValue
)
}
onMonthChange={(month) =>
setDate(
dayjs(date)
.month(month)
.startOf('month')
.format('YYYY-MM-DD') as DateStringValue
)
}
/>
<ScheduleHeader.Next
onClick={() =>
setDate(
dayjs(date)
.add(1, 'month')
.startOf('month')
.format('YYYY-MM-DD') as DateStringValue
)
}
/>
<ScheduleHeader.Today
onClick={() =>
setDate(
dayjs().format('YYYY-MM-DD') as DateStringValue
)
}
/>
</ScheduleHeader>
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
withHeader={false}
/>
</div>
);
}Radius
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView } from '@mantine/schedule';
import { events, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
radius="md"
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}Scroll area props
Use scrollAreaProps to customize the scroll area behavior.
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView } from '@mantine/schedule';
import { events, resources } from './data';
function Demo() {
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
return (
<ResourcesMonthView
date={date}
onDateChange={setDate}
resources={resources}
events={events}
scrollAreaProps={{
scrollbarSize: 10,
offsetScrollbars: true,
type: 'always',
scrollbars: 'x',
}}
startScrollDate={dayjs().format('YYYY-MM-DD')}
/>
);
}