Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

DisplayReact

Scheduler

A calendar of events in time: a day, a work week or a week as a time grid, a month as a grid of days, or the next days as an agenda.

Rendered by React 19.3.1

Loading React…
import * as React from 'react'
import {
  Scheduler,
  type SchedulerEventData,
  type SchedulerView,
} from '@hamur/react/scheduler'
import { Toggle } from '@hamur/react/toggle'
import { ToggleGroup } from '@hamur/react/toggle-group'

const events: SchedulerEventData[] = [
  { id: 'gym', title: 'Gym', start: '2026-11-09T08:00', end: '2026-11-09T09:00' },
  { id: 'design', title: 'Design review', start: '2026-11-10T09:00', end: '2026-11-10T10:30' },
  { id: 'standup', title: 'Standup', start: '2026-11-10T09:30', end: '2026-11-10T10:00' },
  { id: 'lunch', title: 'Lunch', start: '2026-11-11T12:00', end: '2026-11-11T13:00' },
  { id: 'one-on-one', title: '1:1', start: '2026-11-12T11:00', end: '2026-11-12T11:30' },
  { id: 'offsite', title: 'Team offsite', start: '2026-11-13', end: '2026-11-14', allDay: true },
]
const byId = new Map(events.map((event) => [event.id, event]))

export default function SchedulerExample() {
  const [view, setView] = React.useState<SchedulerView>('week')
  return (
    <Scheduler.Root
      className="scheduler"
      view={view}
      events={events}
      locale="en-US"
      today="2026-11-10"
      now="2026-11-10T10:40"
      defaultDate="2026-11-10"
      minTime="08:00"
      maxTime="15:00"
    >
      <div className="scheduler-toolbar">
        <Scheduler.PrevButton className="button button-square">
          <svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24">
            <path d="m15 18-6-6 6-6" />
          </svg>
        </Scheduler.PrevButton>
        <Scheduler.TodayButton className="button">
          Today
        </Scheduler.TodayButton>
        <Scheduler.NextButton className="button button-square">
          <svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24">
            <path d="m9 18 6-6-6-6" />
          </svg>
        </Scheduler.NextButton>
        <Scheduler.Heading className="scheduler-heading" />
        <ToggleGroup
          className="toggle-group"
          aria-label="View"
          value={[view]}
          onValueChange={(next: string[]) => {
            if (next[0]) setView(next[0] as SchedulerView)
          }}
        >
          <Toggle className="toggle" value="day">
            Day
          </Toggle>
          <Toggle className="toggle" value="workWeek">
            Work week
          </Toggle>
          <Toggle className="toggle" value="week">
            Week
          </Toggle>
        </ToggleGroup>
      </div>
      <Scheduler.Grid className="scheduler-grid">
        {(grid) => (
          <React.Fragment>
            <Scheduler.GridHeader>
              <Scheduler.GridRow className="scheduler-row">
                <Scheduler.HeaderCell className="scheduler-header-cell" />
                {grid.days.map((day) => (
                  <Scheduler.HeaderCell
                    key={day.date}
                    date={day.date}
                    className="scheduler-header-cell"
                  />
                ))}
              </Scheduler.GridRow>
              <Scheduler.GridRow className="scheduler-row">
                <Scheduler.RowHeader className="scheduler-row-header" />
                {grid.allDay.map((cell) => (
                  <Scheduler.Slot
                    key={cell.date}
                    date={cell.date}
                    className="scheduler-slot scheduler-all-day"
                  >
                    {cell.events.map((segment) => (
                      <Scheduler.Event
                        key={`${segment.id}@${segment.date}`}
                        event={byId.get(segment.id)!}
                        segment={segment}
                        className="scheduler-event"
                      >
                        {byId.get(segment.id)!.title}
                      </Scheduler.Event>
                    ))}
                  </Scheduler.Slot>
                ))}
              </Scheduler.GridRow>
            </Scheduler.GridHeader>
            <Scheduler.GridBody>
              {grid.rows.map((row, rowIndex) => (
                <Scheduler.GridRow key={row.time} className="scheduler-row">
                  <Scheduler.RowHeader
                    time={row.hourStart ? row.time : undefined}
                    className="scheduler-row-header"
                  />
                  {row.slots.map((slot, dayIndex) => (
                    <Scheduler.Slot
                      key={slot.start}
                      start={slot.start}
                      className="scheduler-slot"
                    >
                      {slot.events.map((segment) => (
                        <Scheduler.Event
                          key={`${segment.id}@${segment.date}`}
                          event={byId.get(segment.id)!}
                          segment={segment}
                          className="scheduler-event"
                        >
                          {byId.get(segment.id)!.title}
                        </Scheduler.Event>
                      ))}
                      {grid.placeholder?.slot === slot.start ? (
                        <Scheduler.Placeholder className="scheduler-placeholder" />
                      ) : null}
                      {rowIndex === 0 && grid.now?.date === grid.days[dayIndex].date ? (
                        <Scheduler.NowIndicator
                          className="scheduler-now"
                        />
                      ) : null}
                    </Scheduler.Slot>
                  ))}
                </Scheduler.GridRow>
              ))}
            </Scheduler.GridBody>
          </React.Fragment>
        )}
      </Scheduler.Grid>
    </Scheduler.Root>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<Scheduler.Root>
  <Scheduler.PrevButton />
  <Scheduler.TodayButton />
  <Scheduler.NextButton />
  <Scheduler.Heading />
  <Scheduler.Grid>
    <Scheduler.GridHeader>
      <Scheduler.GridRow>
        <Scheduler.HeaderCell />
        <Scheduler.RowHeader />
        <Scheduler.Slot>
          <Scheduler.Event />
        </Scheduler.Slot>
      </Scheduler.GridRow>
    </Scheduler.GridHeader>
    <Scheduler.GridBody>
          <Scheduler.Placeholder />
          <Scheduler.NowIndicator />
    </Scheduler.GridBody>
  </Scheduler.Grid>
</Scheduler.Root>

// Not in this example: Scheduler.Agenda, Scheduler.AgendaDay, Scheduler.AgendaHeading, Scheduler.Empty, Scheduler.MoreButton, Scheduler.MoveHandle, Scheduler.ResizeHandle

Examples

Drag

Events moved and resized by the pointer and by their handles; onEventChange writes the change into the app's events.

SchedulerDrag.tsx
import * as React from 'react'
import {
  Scheduler,
  type SchedulerEventChange,
  type SchedulerEventData,
} from '@hamur/react/scheduler'

const initial: SchedulerEventData[] = [
  { id: 'design', title: 'Design review', start: '2026-11-10T09:00', end: '2026-11-10T10:30' },
  { id: 'standup', title: 'Standup', start: '2026-11-10T09:30', end: '2026-11-10T10:00' },
  { id: 'focus', title: 'Focus time', start: '2026-11-11T10:00', end: '2026-11-11T12:00' },
  { id: 'retro', title: 'Retro', start: '2026-11-12T11:00', end: '2026-11-12T12:00' },
]
const at = (value: string) => value.replace('T', ' ')

export default function SchedulerDragExample() {
  const [events, setEvents] = React.useState(initial)
  const [log, setLog] = React.useState('Drag an event, or a handle at its edge.')
  const handleEventChange = (change: SchedulerEventChange) => {
    setEvents((current) =>
      current.map((event) =>
        event.id === change.id ? { ...event, start: change.start, end: change.end } : event,
      ),
    )
    const title = events.find((event) => event.id === change.id)?.title
    setLog(`${title}: ${at(change.start)} to ${at(change.end)}`)
  }
  return (
    <div className="stack">
      <Scheduler.Root
        className="scheduler"
        defaultView="workWeek"
        events={events}
        onEventChange={handleEventChange}
        locale="en-US"
        today="2026-11-10"
        now="2026-11-10T08:20"
        defaultDate="2026-11-10"
        minTime="08:00"
        maxTime="13:00"
      >
        <Scheduler.Grid className="scheduler-grid" aria-label="Team week">
          {(grid) => (
            <React.Fragment>
              <Scheduler.GridHeader>
                <Scheduler.GridRow className="scheduler-row">
                  <Scheduler.HeaderCell className="scheduler-header-cell" />
                  {grid.days.map((day) => (
                    <Scheduler.HeaderCell
                      key={day.date}
                      date={day.date}
                      className="scheduler-header-cell"
                    />
                  ))}
                </Scheduler.GridRow>
              </Scheduler.GridHeader>
              <Scheduler.GridBody>
                {grid.rows.map((row) => (
                  <Scheduler.GridRow key={row.time} className="scheduler-row">
                    <Scheduler.RowHeader
                      time={row.hourStart ? row.time : undefined}
                      className="scheduler-row-header"
                    />
                    {row.slots.map((slot) => (
                      <Scheduler.Slot
                        key={slot.start}
                        start={slot.start}
                        className="scheduler-slot"
                      >
                        {slot.events.map((segment) => {
                          const event = events.find((entry) => entry.id === segment.id)!
                          return (
                            <React.Fragment key={`${segment.id}@${segment.date}`}>
                              <Scheduler.Event
                                event={event}
                                segment={segment}
                                className="scheduler-event"
                              >
                                {event.title}
                              </Scheduler.Event>
                              <Scheduler.MoveHandle
                                event={event}
                                segment={segment}
                                className="scheduler-handle scheduler-move"
                              />
                              <Scheduler.ResizeHandle
                                event={event}
                                segment={segment}
                                edge="start"
                                className="scheduler-handle scheduler-resize"
                              />
                              <Scheduler.ResizeHandle
                                event={event}
                                segment={segment}
                                edge="end"
                                className="scheduler-handle scheduler-resize"
                              />
                            </React.Fragment>
                          )
                        })}
                        {grid.placeholder?.slot === slot.start ? (
                          <Scheduler.Placeholder className="scheduler-placeholder" />
                        ) : null}
                      </Scheduler.Slot>
                    ))}
                  </Scheduler.GridRow>
                ))}
              </Scheduler.GridBody>
            </React.Fragment>
          )}
        </Scheduler.Grid>
      </Scheduler.Root>
      <p className="muted scheduler-log">{log}</p>
    </div>
  )
}

Month

A month that draws two events a day and a MoreButton for the rest, which shows them all.

SchedulerMonth.tsx
import * as React from 'react'
import { Scheduler, type SchedulerEventData } from '@hamur/react/scheduler'

const events: SchedulerEventData[] = [
  { id: 'kickoff', title: 'Kickoff', start: '2026-11-02T09:00', end: '2026-11-02T10:00' },
  { id: 'design', title: 'Design review', start: '2026-11-10T09:00', end: '2026-11-10T10:30' },
  { id: 'standup', title: 'Standup', start: '2026-11-10T09:30', end: '2026-11-10T10:00' },
  { id: 'lunch', title: 'Lunch', start: '2026-11-10T12:00', end: '2026-11-10T13:00' },
  { id: 'demo', title: 'Demo', start: '2026-11-10T16:00', end: '2026-11-10T17:00' },
  { id: 'offsite', title: 'Team offsite', start: '2026-11-12', end: '2026-11-14', allDay: true },
  { id: 'release', title: 'Release', start: '2026-11-19T15:00', end: '2026-11-19T16:00' },
  { id: 'party', title: 'Party', start: '2026-11-27T18:00', end: '2026-11-27T21:00' },
]
const byId = new Map(events.map((event) => [event.id, event]))
const PER_CELL = 2

export default function SchedulerMonthExample() {
  const [expanded, setExpanded] = React.useState<string | null>(null)
  return (
    <Scheduler.Root
      className="scheduler"
      defaultView="month"
      events={events}
      locale="en-US"
      today="2026-11-10"
      now="2026-11-10T10:40"
      defaultDate="2026-11-10"
    >
      <div className="scheduler-toolbar">
        <Scheduler.PrevButton className="button button-square">
          <svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24">
            <path d="m15 18-6-6 6-6" />
          </svg>
        </Scheduler.PrevButton>
        <Scheduler.NextButton className="button button-square">
          <svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24">
            <path d="m9 18 6-6-6-6" />
          </svg>
        </Scheduler.NextButton>
        <Scheduler.Heading className="scheduler-heading" />
      </div>
      <Scheduler.Grid className="scheduler-grid">
        {(grid) => (
          <React.Fragment>
            <Scheduler.GridHeader>
              <Scheduler.GridRow className="scheduler-row">
                {grid.weekdays.map((weekday) => (
                  <Scheduler.HeaderCell
                    key={weekday.index}
                    weekday={weekday.index}
                    className="scheduler-header-cell"
                  />
                ))}
              </Scheduler.GridRow>
            </Scheduler.GridHeader>
            <Scheduler.GridBody>
              {grid.weeks.map((week) => (
                <Scheduler.GridRow key={week.days[0].date} className="scheduler-row">
                  {week.days.map((day) => {
                    const shown = expanded === day.date ? day.events : day.events.slice(0, PER_CELL)
                    return (
                      <Scheduler.Slot key={day.date} date={day.date} className="scheduler-cell">
                        <span className="scheduler-cell-day">{Number(day.date.slice(8))}</span>
                        {shown.map((segment) => (
                          <Scheduler.Event
                            key={`${segment.id}@${segment.date}`}
                            event={byId.get(segment.id)!}
                            segment={segment}
                            className="scheduler-event"
                          >
                            {byId.get(segment.id)!.title}
                          </Scheduler.Event>
                        ))}
                        {shown.length < day.events.length ? (
                          <Scheduler.MoreButton
                            date={day.date}
                            count={day.events.length - shown.length}
                            className="scheduler-more"
                            onClick={() => setExpanded(day.date)}
                          >
                            +{day.events.length - shown.length}
                          </Scheduler.MoreButton>
                        ) : null}
                      </Scheduler.Slot>
                    )
                  })}
                </Scheduler.GridRow>
              ))}
            </Scheduler.GridBody>
          </React.Fragment>
        )}
      </Scheduler.Grid>
    </Scheduler.Root>
  )
}

Agenda

The next seven days as day groups; Enter on an event calls onAction, and an empty range shows Empty.

SchedulerAgenda.tsx
import * as React from 'react'
import { Scheduler, type SchedulerEventData } from '@hamur/react/scheduler'

const events: SchedulerEventData[] = [
  { id: 'standup', title: 'Standup', start: '2026-11-10T09:30', end: '2026-11-10T10:00' },
  { id: 'lunch', title: 'Lunch with Ana', start: '2026-11-10T12:00', end: '2026-11-10T13:00' },
  { id: 'offsite', title: 'Team offsite', start: '2026-11-12', end: '2026-11-13', allDay: true },
  { id: 'brunch', title: 'Brunch', start: '2026-11-14T11:00', end: '2026-11-14T12:00' },
]
const byId = new Map(events.map((event) => [event.id, event]))
const time = (segment: { allDay: boolean; start: string; end: string }) =>
  segment.allDay ? 'All day' : `${segment.start.slice(11)} to ${segment.end.slice(11)}`

export default function SchedulerAgendaExample() {
  const [opened, setOpened] = React.useState('Nothing opened yet.')
  return (
    <div className="stack">
      <Scheduler.Root
        className="scheduler"
        defaultView="agenda"
        events={events}
        locale="en-US"
        today="2026-11-10"
        now="2026-11-10T10:40"
        defaultDate="2026-11-10"
      >
        <div className="scheduler-toolbar">
          <Scheduler.PrevButton className="button button-square">
            <svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24">
              <path d="m15 18-6-6 6-6" />
            </svg>
          </Scheduler.PrevButton>
          <Scheduler.NextButton className="button button-square">
            <svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24">
              <path d="m9 18 6-6-6-6" />
            </svg>
          </Scheduler.NextButton>
          <Scheduler.Heading className="scheduler-heading" />
        </div>
        <Scheduler.Agenda className="scheduler-agenda">
          {(agenda) => (
            <React.Fragment>
              {agenda.days
                .filter((day) => day.events.length > 0)
                .map((day) => (
                  <Scheduler.AgendaDay
                    key={day.date}
                    date={day.date}
                    className="scheduler-agenda-day"
                  >
                    <Scheduler.AgendaHeading className="scheduler-agenda-heading" />
                    {day.events.map((segment) => (
                      <Scheduler.Event
                        key={`${segment.id}@${segment.date}`}
                        event={byId.get(segment.id)!}
                        segment={segment}
                        className="scheduler-event"
                        onAction={() => setOpened(`Opened ${byId.get(segment.id)!.title}`)}
                      >
                        <span className="scheduler-event-time">{time(segment)}</span>
                        {byId.get(segment.id)!.title}
                      </Scheduler.Event>
                    ))}
                  </Scheduler.AgendaDay>
                ))}
              <Scheduler.Empty className="muted">No events in these days.</Scheduler.Empty>
            </React.Fragment>
          )}
        </Scheduler.Agenda>
      </Scheduler.Root>
      <p className="muted scheduler-log">{opened}</p>
    </div>
  )
}

Unavailable

A booking grid whose lunch hour and Friday afternoon are unavailable; a selection reads back from value.

SchedulerUnavailable.tsx
import * as React from 'react'
import {
  Scheduler,
  type SchedulerEventData,
  type SchedulerRange,
} from '@hamur/react/scheduler'

const events: SchedulerEventData[] = [
  { id: 'clinic', title: 'Clinic', start: '2026-11-10T09:00', end: '2026-11-10T11:00' },
  { id: 'rounds', title: 'Rounds', start: '2026-11-12T08:00', end: '2026-11-12T09:00' },
]
const byId = new Map(events.map((event) => [event.id, event]))
// The lunch hour every day, and Friday from noon.
const isSlotUnavailable = (start: string) =>
  start.slice(11, 13) === '12' || (start.startsWith('2026-11-13') && start.slice(11) >= '12:00')
const at = (value: string) => value.replace('T', ' ')

export default function SchedulerUnavailableExample() {
  const [value, setValue] = React.useState<SchedulerRange | null>(null)
  return (
    <div className="stack">
      <Scheduler.Root
        className="scheduler"
        defaultView="workWeek"
        events={events}
        value={value}
        onValueChange={setValue}
        isSlotUnavailable={isSlotUnavailable}
        locale="en-US"
        today="2026-11-10"
        now="2026-11-10T08:20"
        defaultDate="2026-11-10"
        minTime="08:00"
        maxTime="14:00"
      >
        <Scheduler.Grid className="scheduler-grid" aria-label="Book an appointment">
          {(grid) => (
            <React.Fragment>
              <Scheduler.GridHeader>
                <Scheduler.GridRow className="scheduler-row">
                  <Scheduler.HeaderCell className="scheduler-header-cell" />
                  {grid.days.map((day) => (
                    <Scheduler.HeaderCell
                      key={day.date}
                      date={day.date}
                      className="scheduler-header-cell"
                    />
                  ))}
                </Scheduler.GridRow>
              </Scheduler.GridHeader>
              <Scheduler.GridBody>
                {grid.rows.map((row) => (
                  <Scheduler.GridRow key={row.time} className="scheduler-row">
                    <Scheduler.RowHeader
                      time={row.hourStart ? row.time : undefined}
                      className="scheduler-row-header"
                    />
                    {row.slots.map((slot) => (
                      <Scheduler.Slot
                        key={slot.start}
                        start={slot.start}
                        className="scheduler-slot"
                      >
                        {slot.events.map((segment) => (
                          <Scheduler.Event
                            key={`${segment.id}@${segment.date}`}
                            event={byId.get(segment.id)!}
                            segment={segment}
                            className="scheduler-event"
                          >
                            {byId.get(segment.id)!.title}
                          </Scheduler.Event>
                        ))}
                        {grid.placeholder?.slot === slot.start ? (
                          <Scheduler.Placeholder className="scheduler-placeholder" />
                        ) : null}
                      </Scheduler.Slot>
                    ))}
                  </Scheduler.GridRow>
                ))}
              </Scheduler.GridBody>
            </React.Fragment>
          )}
        </Scheduler.Grid>
      </Scheduler.Root>
      <p className="muted scheduler-log">
        {value ? `Selected ${at(value.start)} to ${at(value.end)}` : 'Nothing selected'}
      </p>
    </div>
  )
}

API reference

59 props across 22 parts, in React syntax. className, style, render are listed once, under Shared props.

AgendaScheduler.Agenda

The agenda view: the range's days, each a group of its events. Its children are a function of the agenda's model. Renders a <div>.

Agenda props
PropTypeDefault
childrenThe days of the agenda, a function of its model (SchedulerAgendaData).React.ReactNode | ((agenda: SchedulerAgendaData) => React.ReactNode)—

AgendaDayScheduler.AgendaDay

A day of the agenda, a group named by its AgendaHeading. Renders a <div>.

AgendaDay props
PropTypeDefault
daterequiredThe day, a DateString.string—

AgendaHeadingScheduler.AgendaHeading

The full date of an AgendaDay, which names it. Renders a <h3>.

EmptyScheduler.Empty

Shown only while the range has no events. Renders a <div>.

EventScheduler.Event

An event's segment on one day. Its handles are its siblings, right after it in the same Slot. Renders a <div>.

Event props
PropTypeDefault
eventrequiredThe event, one of the Root's events.SchedulerEvent<unknown>—
segmentThe segment of the event this part draws, from the Grid's or the Agenda's model.SchedulerSegment—
disabledWhether the event cannot be activated, moved or resized.boolean—
onActionCalled when the event is activated by a click, Enter or Space.((details: SchedulerEvent.ActionDetails) => void)—

GridScheduler.Grid

The time grid (day, week, workWeek) or the month grid. Its children are a function of the grid's model, which the app maps to rows, cells and events. Renders a <div>.

Grid props
PropTypeDefault
childrenThe rows of the grid, a function of the grid's model (SchedulerGridData).React.ReactNode | ((grid: SchedulerGridData) => React.ReactNode)—

GridBodyScheduler.GridBody

The rows of slots. Renders a <div>.

GridHeaderScheduler.GridHeader

The rows above the slots: the day headers and the all-day row. Renders a <div>.

GridRowScheduler.GridRow

A row of the grid: the day headers, the all-day row, a row of slots or a week. Renders a <div>.

HeaderCellScheduler.HeaderCell

A column header: a day of the time grid (date), a weekday of the month grid (weekday), or the empty corner over the row headers. Renders a <div>.

HeaderCell props
PropTypeDefault
dateThe day of a time grid column, a DateString.string—
weekdayThe weekday of a month grid column, 0 being Sunday.number—

HeadingScheduler.Heading

The range shown, which names the Root and the Grid and is spoken when it changes. Renders a <h2>.

MoreButtonScheduler.MoreButton

Stands for the events of a day the app does not draw. Its press is the app's. Renders a <button>.

MoreButton props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
daterequiredThe day, a DateString.string—
countrequiredThe number of the day's events the app does not draw.number—

MoveHandleScheduler.MoveHandle

Moves its event by pointer, touch, keys or click-to-place. Rendered right after its Event. Renders a <button>.

MoveHandle props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
eventrequiredThe event whose handle this is.SchedulerEvent<unknown>—
segmentThe segment of the event drawn by the Event this handle follows.SchedulerSegment—

NextButtonScheduler.NextButton

Moves the range forward: a day, a week, a month or the agenda's days. Renders a <button>.

NextButton props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
focusableWhenDisabledWhether the button stays focusable while the Root is disabled.booleantrue

NowIndicatorScheduler.NowIndicator

The current time, rendered by the app in today's first visible slot while now is inside the window (grid.now). Renders a <div>.

PlaceholderScheduler.Placeholder

The box of a pending move, resize or selection, rendered by the app in the Slot the Grid's model names (grid.placeholder.slot). Renders a <div>.

PrevButtonScheduler.PrevButton

Moves the range back: a day, a week, a month or the agenda's days. Renders a <button>.

PrevButton props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
focusableWhenDisabledWhether the button stays focusable while the Root is disabled.booleantrue

ResizeHandleScheduler.ResizeHandle

Moves the start or the end of its event. Rendered right after its Event and MoveHandle. Renders a <button>.

ResizeHandle props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
eventrequiredThe event whose handle this is.SchedulerEvent<unknown>—
segmentThe segment of the event drawn by the Event this handle follows.SchedulerSegment—
edgerequiredThe edge of the event this handle moves."start" | "end"—

RootScheduler.Root

Groups all parts of the scheduler: a calendar of events by day, week, work week, month or agenda, where slots are selected and events are moved and resized. Renders a <div>.

Root props
PropTypeDefault
viewThe view.SchedulerView—
defaultViewThe view when the component is initially rendered.SchedulerView'week'
onViewChangeEvent handler called when actionsRef.setView changes the view.((view: SchedulerView, eventDetails: { reason: "imperative-action"; event: Event; cancel: () => void; allowPropagation: () => void; isCanceled: boolean; isPropagationAllowed: boolean; trigger: Element | undefined; }) => void)—
dateThe anchor of the range shown (YYYY-MM-DD).`${number}-${number}-${number}`—
defaultDateThe anchor of the range when the component is initially rendered.`${number}-${number}-${number}`—
onDateChangeEvent handler called when the range moves: the buttons, Page Up and Page Down, actionsRef.((date: `${number}-${number}-${number}`, eventDetails: SchedulerRootDateChangeEventDetails) => void)—
eventsThe events, never written: a move or a resize is a request through onEventChange.readonly SchedulerEvent<unknown>[][]
onEventChangeEvent handler called once per gesture, at the drop, with the event's new and previous range.((change: SchedulerEventChange, eventDetails: SchedulerRootEventChangeEventDetails) => void)—
valueThe selected range of a new event.SchedulerRange | null—
defaultValueThe selected range when the component is initially rendered.SchedulerRange | nullnull
onValueChangeEvent handler called when a selection is committed.((value: SchedulerRange | null, eventDetails: SchedulerRootValueChangeEventDetails) => void)—
slotDurationThe minutes of a slot (a row of the time grid).number30
snapDurationThe minutes of a drag step and of an arrow key while grabbed.number15
largeStepThe minutes of Shift and an arrow key while grabbed.number60
defaultEventDurationThe minutes a click or Enter on a slot selects.number60
minTimeThe start of each day's visible window.TimeString'00:00'
maxTimeThe end of each day's visible window; '24:00' is the end of the day.TimeString'24:00'
scrollToTimeThe time the Grid scrolls to on mount.TimeString—
agendaDaysThe days the agenda view lists from date.number7
fixedWeeksWhether the month view always shows six weeks.booleanfalse
localeThe locale of the texts and of the week.string'en-US'
firstDayOfWeekThe first day of the week, 0 is Sunday.number | null—
isSlotUnavailableWhether a slot cannot be selected or dropped on; gets its start and exclusive end.((start: string, end: string) => boolean)—
readOnlyWhether nothing can be selected, moved or resized; navigation and onAction work.booleanfalse
disabledWhether the scheduler ignores every interaction but moving focus.booleanfalse
longPressDelayThe milliseconds a touch or pen holds an Event or a Slot before a gesture starts.number250
todayToday (YYYY-MM-DD).`${number}-${number}-${number}`—
nowNow (YYYY-MM-DDTHH:MM).`${number}-${number}-${number}T${number}:${number}${string}`—
timeZoneThe IANA time zone that decides today and now when they are read.string—
labelsThe texts of the parts and the status messages, in English by default.Partial<SchedulerLabels>—
actionsRefA ref to imperative actions.React.RefObject<SchedulerActions | null>—

RowHeaderScheduler.RowHeader

A row's header: the time of the hour's first slot, nothing on the hour's other rows, or the all-day row's “All day” in the GridHeader. Renders a <div>.

RowHeader props
PropTypeDefault
timeThe time of the hour's first slot, a TimeString; the hour's other rows leave it out.string—

SlotScheduler.Slot

A cell of the grid: a timed slot (start), an all-day cell or a month cell (date). Renders a <div>.

Slot props
PropTypeDefault
startThe start of a timed slot, a DateTimeString.string—
dateThe day of an all-day cell or a month cell, a DateString.string—

TodayButtonScheduler.TodayButton

Moves the range to the one that holds today. Renders a <button>.

TodayButton props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
focusableWhenDisabledWhether the button stays focusable while the Root is disabled.booleantrue

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
ArrowLeftArrowRightMoves focus to the same time on the previous or next day; swapped in right to leftSlot
ArrowUpArrowDownMoves focus a slot; in the month view, a week; unavailable slots are skippedSlot
HomeEndMoves focus to the first or last day at the same time; with Ctrl, to the first or last slot of the gridSlot
PageUpPageDownShows the previous or next range at the same slot (onDateChange, reason keyboard)Slot
EnterSpaceSelects defaultEventDuration from the focused slot, or commits a pending rangeSlot
ShiftArrowDownShiftArrowUpExtends or shrinks a pending range by a slotSlot
EscapeClears a pending range; not handled when there is noneSlot
TabMoves from the focused slot into its events and their handles, then out of the gridEvent
EnterSpaceCalls onAction; the arrows keep moving through the gridEvent
EnterSpacePicks the event up (aria-pressed), and drops it on the second pressMoveHandle
Move the event by snapDuration or a day, with Shift by largeStep; Enter drops itMoveHandle
ArrowUpArrowDownMove the start or the end by snapDuration, never past the other edgeResizeHandle
EscapeCancels the move or the resize; nothing is called and focus stays on the handleMoveHandle
TabMoves from event to event; the agenda has no arrow keysEvent

Accessibility

What the parts set
PartSetsSo that
Rootrole="group" named by the Heading; its last child is a visually hidden status regionthe scheduler is one named region, and picks up, moves and drops are announced
Headingh2, aria-live="polite" and aria-atomic, the range as text ("November 8 – 14, 2026")a new range is announced once when the buttons or PageDown change it, and not on the first render
PrevButtona button named from labels by the view ("Previous week", "Next month") when its content has no textan icon-only button says where it goes
NextButtonas PrevButton ("Next week", "Next 7 days")as PrevButton
TodayButtonas PrevButton, labels "Today"as PrevButton
Gridrole="grid" named by the Heading (an aria-label without one), aria-readonly with readOnlythe grid is read by its range, and a read-only one says so
HeaderCellrole="columnheader", the short day as text and the full date as aria-label and abbr, aria-current="date" todaya column is read "Tuesday, November 10", not "Tue 10"
Slotrole="gridcell", roving tabindex, aria-label the date and time with the count of its events, aria-selected, aria-disabled when unavailablethe focused slot says when it is, whether it is chosen and whether it can be
Eventrole="button", aria-label the title, times and date ("Standup, 9:30 AM to 10:00 AM, Tuesday, November 10, 2026"), a Tab stop only in the focused slotan event is read with its time, and the grid stays one Tab stop
MoveHandlea button named "Move event" and its event, aria-pressed while grabbed, touch-action: nonethe drag has a keyboard alternative (WCAG 2.5.7) whose state is read
ResizeHandleas MoveHandle, named "Resize start of event" or "Resize end of event" by edgethe start and the end can each be moved by keys
MoreButtona button named "2 more events on Tuesday, November 10"the hidden events of a month cell are counted with their date
Placeholderaria-hidden="true"the preview of a drag only shows; the status region speaks
NowIndicatoraria-hidden="true"the line of now only shows
AgendaDayrole="group" named by its AgendaHeading (h3)each day of the agenda is a named list of buttons

Focus order

  1. The navigation buttons and the Heading, in the order you render them; the app's view switch where you put it.
  2. The grid is one Tab stop: the last focused slot, else today's slot holding now, else today's first visible slot, else the first slot. Nothing takes focus on mount.
  3. Tab from the focused slot reaches its events, each followed by its handles, then leaves the grid. A drop leaves focus on the handle.
  4. In the agenda every event is a Tab stop, in time order.