Repository navigation
Schedule component rrule issue #9112
Description
Dependencies check up
- I have verified that I use latest version of all @mantine/* packages
What version of @mantine/* packages do you have in package.json?
^9.4.1
What package has an issue?
@mantine/schedule
What framework do you use?
Other, I will specify in the bug description
In which browsers you can reproduce the issue?
None
Describe the bug
the used Framework is : React js
Description:
ScheduleEventData documents start/end/recurrence fields as local, naive datetime strings (YYYY-MM-DD HH:mm:ss, no offset), and isAllDayEvent/isEventInTimeRange classify events purely using local wall-clock comparisons. However, expandRecurringEvents (in utils/expand-recurring-events/expand-recurring-events.ts) builds the rrule dtstart via:
const dtstart = dayjs(event.recurrence?.dtstart || event.start);
options.dtstart = dtstart.toDate();
const rule = new RRule(options);
rrule (v2.8.1) reads the UTC getters off this Date and advances occurrences by fixed 86,400,000ms (24h) increments in absolute time — it does not do "same local wall-clock time, next calendar day" arithmetic. As long as the local UTC offset stays constant across the whole expansion range, this is invisible (every generated instance still lands on local midnight). But if the expansion range crosses a DST transition, the fixed-UTC-ms increments no longer land on local midnight afterward — and the drift is permanent, it does not self-correct.
Concretely, for a daily FREQ=DAILY rule, once the range crosses a DST "fall back" transition:
- The transition-day instance gets a 23-hour span (not 24h to next midnight), fails isAllDayEvent, and renders as a regular timed block instead of all-day.
- Every following instance's local start/end time stays permanently shifted by 1 hour (e.g. 23:00:00 instead of 00:00:00), indefinitely.
Reproduction:
process.env.TZ = 'Africa/Cairo'; // any DST-observing zone works
const dayjs = require('dayjs');
const { expandRecurringEvents } = require('@mantine/schedule/cjs/utils/expand-recurring-events/expand-recurring-events.cjs');
const seedEvent = {
id: 17,
title: 'daily test item',
start: '2026-08-11 00:00:00',
end: '2026-08-12 00:00:00',
color: 'green',
recurrence: { rrule: 'DTSTART:20260811\nRRULE:FREQ=DAILY;INTERVAL=1' },
};
const instances = expandRecurringEvents({
events: [seedEvent],
rangeStart: dayjs('2026-10-25 00:00:00'),
rangeEnd: dayjs('2026-11-02 00:00:00'),
});
instances
.sort((a, b) => dayjs(a.start).valueOf() - dayjs(b.start).valueOf())
.forEach((i) => console.log(i.recurrenceId, '| start:', i.start, '| end:', i.end));
Actual output:
2026-10-28 00:00:00 | start: 2026-10-28 00:00:00 | end: 2026-10-29 00:00:00
2026-10-29 00:00:00 | start: 2026-10-29 00:00:00 | end: 2026-10-29 23:00:00 <- 23h span, not all-day
2026-10-29 23:00:00 | start: 2026-10-29 23:00:00 | end: 2026-10-30 23:00:00 <- starts at 23:00
2026-10-30 23:00:00 | start: 2026-10-30 23:00:00 | end: 2026-10-31 23:00:00 <- stays shifted
2026-10-31 23:00:00 | start: 2026-10-31 23:00:00 | end: 2026-11-01 23:00:00
2026-11-01 23:00:00 | start: 2026-11-01 23:00:00 | end: 2026-11-02 23:00:00
Expected output: every instance stays aligned to local midnight → next local midnight, regardless of any DST transition insid
Suggested fix direction: generate candidate occurrences with calendar-aware local-day arithmetic (e.g. dayjs's .add(n, 'day')clock time across DST) for simple frequencies, or re-normalize each rrule-generated instance back to local midnight when theoriginal anchor was local-midnight-aligned, instead of trusting rrule's raw UTC-interval output as already local.
If possible, include a link to a codesandbox with a minimal reproduction
No response
Possible fix
A few directions, from smallest to most correct:
- Re-normalize rrule's output instead of trusting it directly. rrule correctly determines which calendar days a rule lands on (via its UTC-getter reading of the anchor), but the exact Date instant it returns for each occurrence shouldn't be trusted as-is. For each generated instance, take the UTC year/month/day fields rrule computed, and reconstruct the local datetime from those fields plus the original event's local time-of-day — rather than treating the returned Date as an already-correct local instant.
- Use calendar-aware local stepping for simple frequencies, bypassing rrule entirely. For FREQ=DAILY/WEEKLY/MONTHLY/YEARLY with a plain INTERVAL (the common case), generate occurrences via dayjs(dtstart).add(n, 'day'|'week'|'month'|'year'), which preserves local wall-clock time across DST by design. Fall back to rrule only for patterns that need BYDAY/BYSETPOS/etc., and apply fix Make test change #1 to those.
- Apply the standard rrule.js local-time workaround: never mix real instants with UTC-field values. Construct dtstart so its UTC fields equal the intended local wall-clock values, let rrule iterate (self-consistent either way), then read results back via their UTC fields only — reconstructing the real local Date from those fields directly, rather than using the returned Date's actual instant.
- Adopt rrule's tzid support with a real IANA timezone backend (e.g. luxon). The fully correct fix, but a bigger dependency change — handles edge cases the above can still miss (e.g. a local time that falls inside a DST-skipped hour).
Self-service
- I would be willing to implement a fix for this issue
Activity
[@mantine/schedule] Fix recurring events not working with timzones (#…
rtivital commentedon Aug 22, 2026
Fixed in 9.5.2