Time & Language

Time Zones

Every date the scheduler holds is a ZonedDateTime from @internationalized/date: an instant plus the zone it is read in. The timeZone prop picks the clock the grid is drawn on, and nothing else about your data has to change.

Every Date Is Zoned

Strings you pass in are parsed once, into the zone of the calendar. Dates you get back, in onMutate, in a snippet, in onSelectSlot, are always zoned values in that zone. When timeZone is left out the scheduler uses the zone of the machine it runs on, which on a server rendered page is the server's; see Server Rendering below before relying on the default.

Two Zones, One Set of Events

Both calendars share the same array and the same date. Each names its zone above the hour gutter. Pick another zone for either one and every event moves to its new wall clock time. The buttons jump to the five days of 2026 when clocks change in these zones. Move an event and the log shows the two strings your API could receive for it.

Mar 2 – 8, 2026

EST
Mon 2
Tue 3
Wed 4
Thu 5
Fri 6
Sat 7
Sun 8

Mar 2 – 8, 2026

GMT
Mon 2
Tue 3
Wed 4
Thu 5
Fri 6
Sat 7
Sun 8

What your API receives

  • Move or resize the Design review, or any single event, in either calendar. Occurrences of a series stay where their rule puts them.
<script lang="ts">
  import { parseZonedDateTime } from '@internationalized/date';

  // One array, two calendars. Each shows the same instants on its own clock.
  let events = $state<EventInput[]>([
    {
      id: 'sync',
      title: 'Global sync',
      start: parseZonedDateTime('2026-02-02T10:00[America/New_York]'),
      end: parseZonedDateTime('2026-02-02T11:00[America/New_York]'),
      recurrence: { freq: 'weekly', byDay: [1] }
    }
  ]);
  let date = $state(parseZonedDateTime('2026-03-08T12:00[America/New_York]'));
</script>

<Scheduler bind:events bind:date timeZone="America/New_York" />
<Scheduler bind:events bind:date timeZone="Europe/London" />

Reading Dates In

start and end accept six forms. Five of them name an instant and mean the same thing in every zone. The naive one does not: it is a time on the clock of timeZone, so the instant it stands for changes when the zone does.

Input forms

FormMeansSend it when
'2026-09-16T09:00'9:00 on the clock of timeZone. Changes instant when the zone changesAPIs that store wall clock time for one place
'2026-09-16T02:00Z'An instant, shown in timeZoneAPIs that store UTC. The usual case
'2026-09-16T09:00+07:00'An instant; +0700 works tooAPIs that return the offset they stored
'2026-09-16T09:00[Asia/Ho_Chi_Minh]'An instant, parsed in the named zoneSerialised ZonedDateTime, from toString()
ZonedDateTimeAn instant. For a series, also the zone the rule repeats inDates you build in code, and series that belong to a place
'2026-09-16'Midnight of that day in timeZoneAll-day events, with allDay: true
// Five forms, and what each means. Only the naive one depends on timeZone.
'2026-09-16T09:00'                    // 9:00 on the clock of timeZone; moves when the zone changes
'2026-09-16T02:00Z'                   // an instant, shown in timeZone
'2026-09-16T09:00+07:00'              // an instant; +0700 works too
'2026-09-16T09:00[Asia/Ho_Chi_Minh]'  // an instant, parsed in the named zone
parseZonedDateTime('...')             // an instant; for a series, also the zone it repeats in
'2026-09-16'                          // midnight of that day in timeZone; pair it with allDay

// An API that stores UTC should send the Z form. An API that stores wall clock
// time for a place should send the naive form and let timeZone say which place.

Writing Dates Out

A ZonedDateTime knows how to become any of the forms above. toAbsoluteString() is the one most APIs want.

// Every event the scheduler hands you carries ZonedDateTime values in timeZone
onMutate={(mutation) => {
  const start = mutation.after!.start;
  start.toAbsoluteString();   // '2026-09-16T02:00:00.000Z'                      for an API that stores UTC
  start.toString();           // '2026-09-16T09:00:00+07:00[Asia/Ho_Chi_Minh]'    the instant and its zone
  start.toDate();             // a JS Date, for libraries that want one
  start.hour;                 // 9, on the clock of timeZone
}}

Changing the Zone at Runtime

timeZone is reactive. Change it and the grid redraws on the new clock: the same instants, new wall clock times, a new zone label. With a source, nothing is fetched again; the results already loaded are read in the new zone. The demo has one event of each kind. Switch the zone and watch which one moves.

Wednesday

September 16, 2026

GMT+7
<script lang="ts">
  let zone = $state('Asia/Ho_Chi_Minh');
</script>

<!-- Changing the zone redraws every event on the new clock. Nothing is refetched:
     a source's results are kept as they arrived and read again in the new zone. -->
<Select items={zones} bind:value={zone} />
<Scheduler bind:events timeZone={zone} />

<!-- Which is why the form matters. The same array, seen from London instead of Ho Chi Minh City:
     '2026-09-16T09:00'      still 9:00, now London time, a different instant
     '2026-09-16T04:00Z'     still the same instant, 11:00 there, now shown as 5:00 -->

Days the Clocks Change

The hour axis is the wall clock. 9:00 sits at the same height on every day, including the day a zone skips an hour and the day it repeats one. An event across the change is drawn by its clock times, so the Night shift in the first demo, five real hours from midnight to 6:00 on 8 March in New York, is drawn six hours tall, and the Overnight deploy on 1 November, four real hours from 0:30 to 3:30, is drawn as three. Their real lengths are untouched: move either one to another day and it takes its true duration with it. A drop on a time that does not exist that night, 2:30 on 8 March, lands on 3:30; a drop in the repeated hour on 1 November takes its first occurrence. The zone label follows the change, EDT to EST.

// The axis of the time grid is the wall clock, so 9:00 sits at the same pixel on
// every day, including the two days a year a zone skips or repeats an hour.
// An event across the change is drawn by its clock times, which is what paper
// calendars do, and its real length stays what it was.
{
  id: 'night',
  title: 'Night shift',
  start: parseZonedDateTime('2026-03-08T00:00[America/New_York]'),   // clocks go forward at 2:00
  end: parseZonedDateTime('2026-03-08T06:00[America/New_York]')      // five real hours, drawn as six
}

// Dropping an event onto a time that does not exist that night (2:30) lands it
// on 3:30; a repeated hour resolves to its first occurrence.

Recurring Series Across Zones

A series is anchored to a zone. Given a ZonedDateTime, it repeats in that value's zone, whatever the calendar shows: the Sydney standup in the first demo is 9:00 Sydney every weekday, so in New York it lands at 17:00 or 18:00 the evening before, and Monday's standup shows on Sunday. byDay, until and exDates are all read in the series' zone. A series given as strings repeats in the calendar's zone and moves with it. See Recurring Events.

// A series given as a ZonedDateTime repeats in that zone: 9:00 Sydney every
// weekday stays 9:00 Sydney when the calendar shows New York, where it is 17:00
// or 18:00 by season and Monday's standup falls on Sunday evening.
{
  id: 'sydney',
  title: 'Sydney standup',
  start: parseZonedDateTime('2026-03-02T09:00[Australia/Sydney]'),
  end: parseZonedDateTime('2026-03-02T09:30[Australia/Sydney]'),
  recurrence: { freq: 'weekly', byDay: [1, 2, 3, 4, 5] }
}

// A series given as strings repeats in the calendar's zone, and moves with it.

The Zone Label

The week and day views print the zone's short name, from Intl in the current locale, above the hour gutter: EST, GMT+1, GMT+7. Its title attribute is the IANA id, so hovering shows America/New_York. The event popover adds the same name after the time when the calendar's zone is not the device's, so a reader in Ho Chi Minh City looking at a New York calendar sees "(EST)" and knows the hour is not theirs. All of these carry data-sch-zone, the hook for hiding or restyling them; see Theming.

Server Rendering

Set timeZone explicitly on any page rendered on the server. Take it from the user's profile, a cookie, or the organisation's zone, so the server and the browser draw the same calendar. Without it the server uses its own zone and the browser the reader's, and the two markups disagree on where the days begin. If the device zone is what you want, render with a fixed zone and switch after mount, the way the first demo adds "Your zone" to its list.

<!-- +page.server.ts: the zone comes from the user, not from the server -->
export const load = async ({ locals, cookies }) => ({
  timeZone: locals.user?.timeZone ?? cookies.get('tz') ?? 'UTC'
});

<!-- +page.svelte -->
<Scheduler bind:events timeZone={data.timeZone} />

<!-- Without it the server renders in its own zone and the browser in the reader's;
     the day boundaries and the current-time line then disagree, and hydration
     repaints the calendar. -->

Only One Zone per Grid

A grid has one hour axis, so one zone. There is no second gutter for another zone. To show two at once, render two schedulers with the same bind:events and bind:date, as the first demo does. Both stay in step, and a move in one is a move in the other.

Props

The props this page uses.

PropTypeDefault
timeZoneTimeZoneIdlocal zone
eventsEventInput<T>[]-
dateZonedDateTimetoday
localestring'en-US'
sourceEventSourceFn<T>-