InteractionApplies toLite and Pro
Keyboard, accessibility and touch
In Pro, set keyboardEnabled (off by default), add keyboardMode: 'Full' for the complete key set and keyboardTarget: 'component' so keys act only while the grid has focus. The grid is a single tab stop with role="grid": focus moves through aria-activedescendant and changes are announced in nine languages. Lite has arrow-key navigation built in. On touch screens, hold an event to move it and drag its handles to resize it.
A resource scheduler is a large two-dimensional grid, which makes keyboard and screen reader support harder than in a list or a form. SuperScheduler gives the grid a single tab stop, a roving focus that survives virtualization, spoken announcements for focus and changes, and keyboard equivalents for moving and resizing events. This guide explains what each edition does, how to turn it on, and what your application still has to provide.
Lite and Pro at a glance
Lite (super-scheduler-lite) | Pro (super-scheduler) | |
|---|---|---|
| Keyboard | Always on: arrows move the active cell, Enter or Space activates it | Off by default; keyboardEnabled, plus keyboardMode: 'Full' for the complete model |
| Events | Native buttons: Tab reaches them, Enter or Space clicks them | Reached with the arrow keys inside the grid; Enter runs the click flow |
| Editing by keyboard | No (read-only edition) | Move with Alt+arrows, resize with Alt+Shift+Left/Right (Full mode) |
| Announcements | No | Focus, selection and committed changes, in nine languages |
| Grid name | ariaLabel option (default "Resource schedule") | Built-in "Scheduler" ("Planificador" for Spanish locales) |
| Touch | Native scrolling and taps | Hold to move, handles to resize, pinch to zoom |
Enable the keyboard in Pro
Pro keeps the keyboard off until you set keyboardEnabled: true. The default keyboardMode: 'SuperScheduler' handles arrows, Enter and Shift+Left/Right. keyboardMode: 'Full' adds the rest of the model: Home/End, PageUp/PageDown, Space, moving and resizing events, the context menu key and announcements of help text. Full mode without keyboardEnabled does nothing and warns in development.
keyboardTarget decides where keys are heard. The default, 'document', reacts to keys pressed anywhere on the page outside text fields, which takes the arrow keys away from page scrolling. Use 'component' so keys act only while the grid has focus; it is also the right choice with several schedulers on one page.
import { useMemo } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import 'super-scheduler/styles.css'
// Module-level: the same object on every render.
const KEYBOARD: SchedulerProps = {
keyboardEnabled: true,
// Keys act only while the grid has focus; the page keeps its own arrow-key scrolling.
keyboardTarget: 'component',
keyboardMode: 'Full',
keyboardOptions: { pageRows: 10, zoomKeys: true },
}
const UNDER_MAINTENANCE = new Set<SuperScheduler.ResourceId>(['room-104'])
export function AccessiblePlanner(props: {
resources: SuperScheduler.ResourceData[]
events: SuperScheduler.EventData[]
onOpen: (id: SuperScheduler.EventId) => void
}) {
const { onOpen } = props
const events = useMemo(() => props.events.slice(), [props.events])
const config = useMemo<SchedulerProps>(
() => ({
...KEYBOARD,
// Enter on a focused event runs the same click flow as the pointer.
onEventClick: (args) => onOpen(args.e.id()),
// Alt + arrow moves go through the same rules as drags.
onEventMoving: (args) => {
if (UNDER_MAINTENANCE.has(args.resource)) {
args.allowed = false
args.message = 'Room under maintenance'
}
},
}),
[onOpen],
)
return (
// The grid's own accessible name is generic: label the region around it.
<section aria-labelledby="room-plan-title">
<h2 id="room-plan-title">Room plan, October 2026</h2>
<SuperSchedulerComponent
{...config}
startDate="2026-10-01"
days={31}
scale="Day"
locale="en-us"
resources={props.resources}
events={events}
/>
</section>
)
}You should be able to Tab into the grid, move with the arrows, press Enter on an event to open it, and press Alt+Down on an event to start moving it. Moving it onto room 104 and pressing Enter announces "Not allowed here", and the event stays where it was.
keyboardOptions tunes Full mode:
| Option | Default | Effect |
|---|---|---|
pageRows | rows in view minus one | Rows moved by PageUp and PageDown |
contextMenuKey | true in Full mode | Menu key and Shift+F10 open the focused item's menu |
bubbleOnFocus | false | Shows the event's bubble while it has keyboard focus |
selectAll | true in Full mode | Ctrl/Cmd+A selects every event in view (needs allowMultiSelect) |
zoomKeys | false | Ctrl/Cmd with = or +, - and 0 zoom in, out and back |
zoomKeys is off by default so the browser's own page zoom shortcuts keep working.
Keys
| Keys | Mode | What happens |
|---|---|---|
| Arrow keys | both | Move the focus. Left and Right stop at each event and each empty cell of the row; Up and Down change rows. |
| Enter | both | On an event: the click flow (onEventClick, then eventClickHandling). On a cell: selects it as a time range. |
| Shift+Left / Shift+Right | both | Extends a time range from the focused cell; releasing Shift selects it. |
| Space | Full | On a cell: adds it to or removes it from the selection. On an event: same as Enter. |
| Home / End | Full | First or last cell of the row; with Ctrl/Cmd, the first or last row. |
| PageUp / PageDown | Full | Moves the focus by pageRows. |
| Alt+arrow keys | Full | Starts moving the focused event; arrows move it, Enter or Space drops it. |
| Alt+Shift+Left / Right | Full | Starts resizing the focused event's end; Left and Right change it, Enter confirms. On a column title, moves the column. |
| Escape | both | Cancels a keyboard move, resize or range. During a keyboard move or resize, Tab also cancels it and leaves the grid. |
| Menu key, Shift+F10 | Full | Opens the menu of the focused event, row header or cell. |
| Ctrl/Cmd+A | Full | Selects every event in view. |
Focus model and screen readers
The Pro grid has role="grid" with aria-rowcount and aria-colcount; row headers are rowheader, time header cells columnheader, and cells and events gridcell. Rows and cells outside the viewport are not in the DOM, so focus does not move from element to element. Instead:
- the grid root is the single tab stop (
tabindex="0") while the keyboard is enabled; - the focused cell or event is a focus node that the root points at with
aria-activedescendant, and it survives scrolling and virtualization; - the focus node's label reads like "Room 101, Oct 1" for a cell and "Ana, Room 101, Oct 2 – 4" for an event.
An event's name comes from its ariaLabel, then its text, then its html as plain text, then its id. A row's name is its resource name. When html shows something different from text, set ariaLabel in onBeforeEventRender:
import { SuperScheduler } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'
type Visit = { patient: string; kind: 'checkup' | 'surgery'; color: string }
const KIND_LABEL: Record<Visit['kind'], string> = { checkup: 'check-up', surgery: 'surgery' }
/** Pass as `onBeforeEventRender` (module-level, so its identity never changes). */
export const labelVisit: NonNullable<SchedulerProps['onBeforeEventRender']> = (args) => {
const visit = args.data as SuperScheduler.EventRenderData<Visit>
args.data.backColor = visit.color
// Dark text on light fills and light text on dark ones (WCAG contrast).
args.data.fontColor = SuperScheduler.ColorUtil.contrasting(visit.color)
// `html` is trusted markup: escape what users typed.
args.data.html = `<strong>${SuperScheduler.Util.escapeHtml(visit.patient)}</strong>`
// The accessible name of the event. Focus labels and announcements append
// the row name and the dates, so they are not repeated here.
args.data.ariaLabel = `${visit.patient}, ${KIND_LABEL[visit.kind]}`
}Because focus labels and announcements append the row and the dates, ariaLabel should hold only what identifies the event. SuperScheduler.ColorUtil.contrasting(color) returns dark text for light fills and light text for dark ones.
The grid's own accessible name is "Scheduler" ("Planificador" when the locale starts with es), and 0.1.0 has no option to change it. Put the scheduler inside a region labelled by a visible heading, as the setup snippet does.
From code, control.keyboard offers focusEvent(e or id), focusCell(date, resource), getFocus(), move(direction), clearFocus() and resetFocus(). onKeyboardFocusChange (cancelable) and onKeyboardFocusChanged report focus changes with previous and focus ({ e } or { cell }); use them to sync a detail panel with the keyboard.
Announcements
A polite live region inside the grid announces:
- in Full mode, a short help text the first time the grid takes focus;
- selections ("Selected: Room 101, Oct 4"), deselections and "N events selected";
- the start of a keyboard move or resize, with instructions;
- committed moves and resizes ("Event moved to Room 101, Oct 3 – 5"), whether they came from the keyboard or the pointer;
- "Cancelled", "Not allowed here" and column moves.
The texts follow the first segment of the scheduler's locale: English, Spanish, Catalan, Basque, Galician, German, French, Italian and Portuguese. Other languages fall back to English. Language packs other than English and Spanish load on demand.
The context menu key
In Full mode, the Menu key and Shift+F10 open the menu of the focused item when it is a SuperScheduler.Menu: the event's contextMenu (or the control's contextMenu), contextMenuResource on a row header, and contextMenuSelection on a cell. If your application draws its own menu from onEventRightClick, handle the key yourself in onKeyDown:
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
export interface MenuRequest {
readonly eventId: SuperScheduler.EventId
/** Viewport coordinates where the application opens its own menu. */
readonly x: number
readonly y: number
}
/**
* Opens the application's menu from the pointer (right click) and from the keyboard
* (Menu key, Shift+F10). The library's Menu key support covers `SuperScheduler.Menu`
* objects only, so a custom menu handles the key in `onKeyDown`.
*/
export function menuHandlers(open: (request: MenuRequest) => void): SchedulerProps {
return {
onEventRightClick: (args) => {
args.preventDefault()
open({ eventId: args.e.id(), x: args.originalEvent.clientX, y: args.originalEvent.clientY })
},
onKeyDown(args) {
const key = args.originalEvent
if (key.key !== 'ContextMenu' && !(key.key === 'F10' && key.shiftKey)) return
const focused = this.keyboard.getFocus().e
const root = key.target
if (focused === undefined || !(root instanceof HTMLElement)) return
// Skips the library's own handling of the key.
args.preventDefault()
// The grid points at the focused item with aria-activedescendant.
const ring = root.ownerDocument.getElementById(
root.getAttribute('aria-activedescendant') ?? '',
)
const box = (ring ?? root).getBoundingClientRect()
open({ eventId: focused.id(), x: box.left, y: box.bottom })
},
}
}Spread menuHandlers(open) into the scheduler props, memoized. Your menu is then responsible for its own focus: move focus into it when it opens, and return it to the grid when it closes.
Accessible alternatives to dragging
WCAG 2.2 success criterion 2.5.7 (↗) asks for a way to do with single pointer actions what dragging does. Full keyboard mode covers keyboard users, but not someone using a single pointer, a switch or voice control. Give every event a non-drag path, for example a detail panel or a context menu entry with start, end and resource fields that updates your state (or calls control.events.update with a new object). Run the same validation you use in onEventMoving before saving, so both paths enforce the same rules.
Touch
On touch screens, one finger scrolls the timeline. The rest of the touch model:
- Moving an event. Hold the event still for
tapAndHoldTimeout(300 ms) and then drag. A finger that moves more than about 8 px before that scrolls instead.eventTapAndHoldHandlingdecides what a hold does:'Move'(default),'ContextMenu'(opens the event'sSuperScheduler.Menu) or'Disabled'. Holding a row header openscontextMenuResource. - Resizing. A tap on an event shows its handles, with 44 px touch targets (
--super-scheduler-handle-target); drag a handle to resize. The edge of an event under a finger moves it rather than resizing it. - Selecting time. A tap on an empty cell selects it (
origin: 'click'); a hold and drag selects a range. - Zoom. Two fingers pinch to zoom (
zoomGesture.pinch, on by default). A second finger cancels any drag in progress. - Hover. Touch has no hover. Hover cards from
eventHovercan be pinned with a tap (pin: 'click'); areas withvisibility: 'TouchVisible'stay visible on touch devices, while'Hover'areas do not appear.
Lite is read-only: it scrolls natively and reports taps through onEventClick and onTimeRangeClick.
Reduced motion, contrast and forced colors
Pro reads the user's preferences through CSS and media queries, with no option to set:
prefers-reduced-motion: reducesets--super-scheduler-durationto0s, makescontrol.zoom.animateTo()instant and removes hover card transitions;prefers-contrast: morestrengthens borders, grid lines, row lines and the selection outline;forced-colors: activeswitches the theme to system colors (Highlight,CanvasText,GrayText) and drops decorative shading such as weekends and today.
Lite also adapts its borders and focus outline to forced colors. If you replace colors with your own tokens or CSS, test these modes again: your overrides can undo them.
Lite keyboard support
Lite needs no configuration. The grid is focusable, aria-readonly, and named by the ariaLabel option. Arrow keys move the active cell (announced through aria-activedescendant), Enter or Space calls onTimeRangeClick for it, and each event is a native button.
import { SuperSchedulerComponent } from 'super-scheduler-lite'
import type { SuperScheduler } from 'super-scheduler-lite'
import 'super-scheduler-lite/styles.css'
const ROOMS: SuperScheduler.ResourceData[] = [
{ id: 'r101', name: 'Room 101' },
{ id: 'r102', name: 'Room 102' },
]
const STAYS: SuperScheduler.EventData[] = [
{ id: 'b1', resource: 'r101', start: '2026-10-02', end: '2026-10-05', text: 'Lena Fischer' },
]
export function OccupancyBoard(props: {
onOpenBooking: (id: SuperScheduler.EventData['id']) => void
onOpenDay: (resource: SuperScheduler.ResourceData['id'], day: string) => void
}) {
return (
<SuperSchedulerComponent
// The grid's accessible name (default "Resource schedule").
ariaLabel="Room occupancy, October 2026"
startDate="2026-10-01"
days={31}
scale="Day"
resources={ROOMS}
events={STAYS}
// Events are native buttons: Tab reaches them, Enter and Space click them.
onEventClick={({ e }) => props.onOpenBooking(e.data.id)}
// Arrow keys move the active cell; Enter or Space activates it.
onTimeRangeClick={({ start, resource }) =>
props.onOpenDay(resource, start.toString('yyyy-MM-dd'))
}
/>
)
}What your application must still ensure
The library handles the grid. These parts belong to your application:
- Contrast. Event colors you set with
backColor,fontColor, CSS or custom content must meet contrast requirements in light and dark mode. - Names in custom content. HTML from
onBeforeEventRender, React slots and row header markup are yours: keeptextmeaningful or setariaLabel, give icons text alternatives, and avoid interactive controls inside event content. - Menus, dialogs and panels. Focus management, labels and Escape handling for anything you open from the grid.
- A non-drag path for every drag action, as described above.
- Page structure. A heading or label for the region around the grid, and a sensible place for it in the tab order.
- Testing. Check your configuration with a screen reader and an automated checker; overrides and custom content can change what users hear.
Related
- Resource trees, columns and selection covers what Space, Enter and Ctrl/Cmd+A select.
- Drag, resize and business rules explains the rules keyboard moves go through.
- Themes, tokens, Tailwind and dark mode covers focus and selection colors.