Data display
Scheduler
A resource scheduler — people, rooms or machines down the side, time across the top.
Import
import { Scheduler } from '@the_viveksingh/vivek-ui'A resource timeline nobody else gives you for free
Rooms, people or machines down the side and time across the top. shadcn/ui, Mantine and Radix ship nothing like it, and MUI puts theirs behind a paid licence. Overlapping bookings stack into lanes so a double-booking is visible rather than hidden underneath.
Tab into the board and use the arrow keys: left and right walk this resource in time order, up and down jump to the nearest booking on the resource above or below. Every booking carries its resource, its times and its duration in its accessible name, because a timeline says all of that through position alone.
- Nothing scheduled
Nothing selected yet. Click or press Enter on a booking.
const resources = [
{ id: 'studio-a', label: 'Studio A', sublabel: 'Ground floor - 12 seats' },
{ id: 'studio-b', label: 'Studio B' },
]
const events = [
{ id: '1', resourceId: 'studio-a', title: 'Standup', start: at(9), end: at(9, 30) },
{ id: '2', resourceId: 'studio-a', title: 'Podcast', start: at(10), end: at(12, 30), tone: 'accent' },
// Overlaps the podcast, so it is packed into a second lane instead of being hidden.
{ id: '3', resourceId: 'studio-a', title: 'Mic check', start: at(11, 30), end: at(12) },
{ id: '4', resourceId: 'studio-b', title: 'Maintenance', start: at(13), end: at(16), tone: 'warning' },
]
<Scheduler
resources={resources}
events={events}
label="Studio bookings, 12 March"
start={at(9)}
end={at(18)}
// Nothing is mutated for you - the board reports, your state decides.
onEventSelect={(event) => setSelected(event)}
/>The keyboard model, which is the whole point
A timeline conveys everything through position, and position is invisible to a screen reader. So the board is one tab stop with a roving focus, and every booking carries its resource, its times and its duration in its accessible name: "Podcast. Studio A, 10:00 to 12:30, 2 hours 30 minutes."
Tab into the board and use the arrow keys: left and right walk this resource in time order, up and down jump to the nearest booking on the resource above or below. Every booking carries its resource, its times and its duration in its accessible name, because a timeline says all of that through position alone.
- Nothing scheduled
Nothing selected yet. Click or press Enter on a booking.
// Left / Right - previous / next booking for this resource, in time order
// Up / Down - the nearest booking in time on the resource above / below
// Home / End - first / last booking for this resource
// Enter, Space - select
// Empty resources are skipped by Up and Down: stopping on a row with nothing
// in it reads as a dead key.
<Scheduler resources={resources} events={events} label="Bookings" />The current-time marker, and why it is opt-in
Reading the clock during render gives the server one marker position and the browser another, which React reports as a hydration mismatch. So the component never does it: showNow reads the clock in an effect after mount, and now takes an explicit time for tests and demos.
Tab into the board and use the arrow keys: left and right walk this resource in time order, up and down jump to the nearest booking on the resource above or below. Every booking carries its resource, its times and its duration in its accessible name, because a timeline says all of that through position alone.
- Nothing scheduled
Nothing selected yet. Click or press Enter on a booking.
// Reads the clock after mount, then ticks once a minute.
<Scheduler resources={resources} events={events} label="Today" showNow />
// Or pin it, which is what the demo above does so the docs never shift.
<Scheduler resources={resources} events={events} label="Today" now={at(13, 20)} />
// Times are written by a deterministic HH:MM formatter rather than
// Intl.DateTimeFormat, whose output varies between Node builds and browsers.
// Pass your own for a 12-hour clock:
<Scheduler
resources={resources}
events={events}
label="Today"
formatTime={(d) => d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
/>Props
Generated from the package's own type declarations, so this table cannot drift from the code.
| Prop | Type | Default | Description |
|---|---|---|---|
resources required | readonly SchedulerResource[] | — | — |
events required | readonly SchedulerEvent[] | — | — |
label required | string | — | Required. A timeline with no accessible name is one more unlabelled region. |
start | Date | number | — | Window start. Defaults to the earliest event, floored to the step. |
end | Date | number | — | Window end. Defaults to the latest event, ceiled to the step. |
step | number | — | Minutes between axis ticks. Default 60. |
minTickWidth | number | — | Minimum pixels per tick. Below this the timeline scrolls rather than crushing. |
showNow | boolean | — | Draw the current-time marker. Left to itself this component never reads the clock during render — that would produce a different marker on the server and the client, which React reports as a hydration mismatch. The clock is read in an effect, after mount. |
now | Date | number | — | An explicit "now", which overrides {@link showNow}'s clock. Useful in tests and demos. |
onEventSelect | (event: SchedulerEvent) => void | — | — |
renderEvent | (event: SchedulerEvent, resource: SchedulerResource) => ReactNode | — | Rendered instead of the default title + time. The wrapper button stays ours. |
formatTime | (value: Date) => string | — | How a time is written, in the axis and in every accessible name. The default is a deterministic 24-hour `HH:MM` rather than `Intl.DateTimeFormat`, because ICU output varies between Node builds and browsers — the same code would render differently for two of your users. Pass your own for 12-hour clocks or other locales. |
className | string | — | — |
Rendering
Scheduler declares 'use client' because it needs state, effects or event handlers. Importing it into a Server Component creates a client boundary at this component — everything above it stays on the server.