Timeline view
Resources as rows on a horizontal time axis, with stacked events.
Resources as rows on a horizontal time axis. Overlapping events in a row stack, and the row grows to fit them. An event that crosses midnight is one bar.
Loading the preview…
import { timelineView } from '@calendar-tsx/core';
timelineView({ days: 7, groupBy: 'resource' }); // a week, one row per resource
timelineView({ days: 1, groupBy: 'resource', slotMinutes: 30 }); // a day
timelineView({ period: 'month', groupBy: 'resource', slotMinutes: 1440, minSlotWidth: '6rem' });
timelineView({ period: 'year', slotMinutes: 1440, minSlotWidth: '6rem' });Nested resources
Set a resource's parentId to another resource ID. Parents remain schedulable resources. Events, backgrounds and
availability use direct resource IDs; collapsing a parent hides its descendants without moving their events into
the parent row.
Register resourceExpansionFeature to enable expansion actions. Resources start expanded by default:
import { calendarFeatures, calendarViews, resourceExpansionFeature, timelineView } from '@calendar-tsx/core';
const features = calendarFeatures({ tree: resourceExpansionFeature({ initiallyExpanded: true }) });
const views = calendarViews({
features,
views: {
timeline: timelineView({ days: 7, groupBy: 'resource' }),
compact: timelineView({ days: 7, groupBy: 'resource', features: { tree: { initiallyExpanded: false } } }),
},
});Pass features and views to the calendar. Call calendar.api.resourceExpansion.toggle(id) or
setExpanded(id, boolean). Leaf actions do nothing; unknown IDs throw. State is a resourceExpansion map of
resource IDs to Boolean overrides. Omitted IDs use that view's initiallyExpanded default. Overrides persist by
ID across navigation, view switches and resource replacement, including while an ancestor is collapsed.
For controlled expansion, pass state: { resourceExpansion } and apply the map emitted through
onResourceExpansionChange. Rows change after acknowledgment. External stores use atoms: { resourceExpansion }.
Without the feature, hierarchical Timeline rows remain fully expanded.
Render TimelineViewResourceToggle inside TimelineViewResource, passing row and the typed calendar instance.
The native button exposes aria-expanded, supplies an accessible label, and renders only for parents. Supply its
visible text or icon as children. preventDefault() skips the action. The existing Timeline examples show this
composition for React and Solid. Labels and content remain consumer-owned. The parts retain the labelled-region
semantics and native tab order; they do not implement treegrid arrow-key navigation.
Expansion affects only Timeline. Hidden resources remain in scheduling constraints. Hit detection and draft previews use visible rows; programmatic operations can still target a hidden resource. An active preview disappears if its target is collapsed and returns if expanded again.
Settings
Every view factory takes exactly one of days, weeks, months or period. Counts are positive integers.
| Duration | Range | next() / prev() |
|---|---|---|
days: N | N dates from date; 7 keeps week alignment. | N days. |
weeks: N | N whole weeks from firstDayOfWeek. | N weeks, keeping the weekday. |
months: N | N calendar months from the first of the anchor month. | N months, landing on the first day. |
period | 'month', 'quarter' or 'year', aligned to its calendar boundary. | One period, landing on its first day. |
| Setting | Type | Default | Description |
|---|---|---|---|
groupBy | 'resource' or ['resource'] | none | One row per resource. Without it, the layout has one row. |
slotMinutes | number | 60 | Positive integer slot length; the final slot may be shorter. |
minSlotWidth | string | none | Passed through as the slot columns' minimum width, such as '3rem'. |
minEventMinutes | number | half of slotMinutes | Minimum display duration, also used for stacking. 0 turns it off. |
hiddenDays | ISO weekdays | none | See hidden weekdays. |
timeWindow | { startMinutes, endMinutes } | [0, 1440) | See visible hours. |
Set minSlotWidth on month and year timelines to keep daily columns readable; the supplied themes then scroll the
timeline horizontally.
- An event appears in every row whose resource it names.
- All-day events have no separate row. They stack with timed events, from the start of their first day to the start of the day after their last.
- A resource's availability is shaded as in the resource view.
- Background events shade the track without taking stack rows. Register
backgroundFeatureto add background layers.
Brief events
A brief timed event is displayed at least minEventMinutes long, half a slot by default, measured in visible clock
minutes across hidden dates and daylight-saving changes. With daily slots and minSlotWidth: '6rem', a bar is then at
least 3rem wide. Stacking uses the enlarged span, so neighboring brief events stay separately selectable. The event
keeps its real start and end for labels. Bars clip at the visible range's end, where one can be narrower than the
minimum. All-day events keep their date spans. Keep CSS padding and borders inside the computed width: a CSS-only
minimum would enlarge bars without restacking them.
Hidden weekdays
hiddenDays omits ISO weekdays. Use the exported DayOfWeek constants (DayOfWeek.Monday is 1, DayOfWeek.Sunday
is 7) or the numbers; the same values work for firstDayOfWeek and weekly background daysOfWeek.
import { DayOfWeek, timeGridView } from '@calendar-tsx/core';
timeGridView({ weeks: 1, hiddenDays: [DayOfWeek.Saturday, DayOfWeek.Sunday] }); // a workweekHidden weekdays change neither the nominal period, the inclusive fetch range nor the navigation step. Hiding all
seven weekdays throws. When every date in the range is hidden, such as a one-day view on a hidden Saturday, the range
shows its dates anyway.
Visible hours
timeWindow limits the clock hours shown, in integer clock minutes: 0 <= startMinutes < 1440 and
startMinutes < endMinutes <= startMinutes + 1440. The default is [0, 1440).
timeGridView({ days: 7, timeWindow: { startMinutes: 480, endMinutes: 1080 } }); // 08:00 to 18:00
timelineView({ days: 1, timeWindow: { startMinutes: 1320, endMinutes: 1800 } }); // 22:00 to 06:00An overnight window belongs to its starting date, so hiding Tuesday still shows Monday's Tuesday-morning portion.
All-day items keep their calendar dates. slotMinutes need not divide the window; the final slot keeps its actual
size, and each slot's dayOffset labels after-midnight times. Fetch events for navigation's inputRange, which adds
the following date when an overnight window needs it.
Example
useLayout narrows the layout for <Show>; see narrowing the layout.
export const Timeline = (props: { calendar: DayCalendar }) => {
const timeline = useLayout('timeline');
return (
<Show when={timeline()}>
{timeline => (
<TimelineView model={timeline()}>
<TimelineViewHeader>
<For each={timeline().days}>
{day => (
<TimelineViewDayHeader day={day}>
<TimelineViewDayTrigger calendar={props.calendar} day={day} view='day'>
{day.date.toLocaleString('en-US', { weekday: 'short', day: 'numeric' })}
</TimelineViewDayTrigger>
</TimelineViewDayHeader>
)}
</For>
<Show when={timeline().days.some(day => day.colSpan > 1)}>
<For each={timeline().slots}>
{slot => (
<TimelineViewSlotLabel slot={slot}>
{slot.time.toLocaleString('en-US', { hour: 'numeric', minute: '2-digit' })}
</TimelineViewSlotLabel>
)}
</For>
</Show>
</TimelineViewHeader>
<TimelineViewBody>
<For each={timeline().rows}>
{row => (
<TimelineViewRow row={row}>
<TimelineViewResource row={row}>
<Show when={props.calendar.api.resourceExpansion}>
{expansion => (
<TimelineViewResourceToggle row={row} calendar={{ api: { resourceExpansion: expansion() } }}>
<span aria-hidden='true'>{row.isExpanded ? '−' : '+'}</span>
</TimelineViewResourceToggle>
)}
</Show>
{row.resource?.title}
</TimelineViewResource>
<TimelineViewLane row={row}>
<For each={row.backgrounds}>{background => <TimelineViewBackground background={background} />}</For>
<For each={row.backgroundEvents}>
{segment => <TimelineViewBackgroundEvent segment={segment} />}
</For>
<For each={row.unavailable}>{segment => <TimelineViewUnavailable segment={segment} />}</For>
<For each={row.segments}>
{segment => (
<TimelineViewEvent
segment={segment}
title={formatEvent(segment.event)}
aria-label={formatEvent(segment.event)}
>
{segment.event.data.title}
</TimelineViewEvent>
)}
</For>
</TimelineViewLane>
</TimelineViewRow>
)}
</For>
<TimelineViewNowIndicator model={timeline()} />
</TimelineViewBody>
</TimelineView>
)}
</Show>
);
};Model
layout is { kind: 'timeline', coordinates, slotUnits, slotMinutes, hourOffset, days, slots, rows, now, minSlotWidth }.
Every start and span is a fraction of the concatenated visible windows. Dates keep equal widths across
daylight-saving transitions.
Time grids and timelines use a fixed clock axis in timeZone: 09:00 sits at the 09:00 position on daylight-saving
days too, and missing hours stay on the axis. The two occurrences of a repeated hour share clock positions;
overlapping events then take separate lanes or stack rows. A bar that crosses a transition encloses every clock
position its real interval visits, and segment.event keeps the real timestamps and offsets. Each timed segment's
fragments keeps its exact coverage (offsets, occurrences and the reason each fragment stops) apart from that
enclosing rectangle.
The core builds one coordinate plan (layout.coordinates) per stable view definition and reuses it across event,
resource and clock updates. Navigation, timeZone, firstDayOfWeek, hiddenDays or timeWindow changes rebuild it.
daysholds one{ key, date, isToday, colStart, colSpan, start, span }per day, for the header.colStartandcolSpancount slot columns.slotsholds{ key, date, time, dayOffset, col, start, span }per slot. Fractional bounds give partial slots their actual width.slotMinutesis each slot's length, andhourOffsetthe slot units from each day's window start to its first hour.rowsholds one row per resource, or a single row withoutgroupBy.nowis the current time's fraction of the visible scale, omitted on hidden dates and outside the range.
| Row field | Description |
|---|---|
resource | The resource; absent without groupBy. |
depth | Zero-based depth in the resource hierarchy. |
hasChildren | Whether the resource has direct children. |
isExpanded | Whether descendants can be visible; false for leaves. |
segments | The row's events. |
backgroundEvents | Timed and all-day background-event coverage, without stack rows. |
backgrounds | Layer coverage, only with backgroundFeature. |
rows | Stack depth: how many events stack at the busiest point. |
unavailable | Fractions of the range outside the resource's availability. |
isUnavailable | The resource has no availability in the range. |
| Segment field | Description |
|---|---|
event | The event, in the calendar's time zone. |
start, span | Display position and duration as fractions of the visible range. |
fragments | Exact coverage before clock enclosure and minimum sizing. |
row | Zero-based stack row; each event takes the lowest row free for it. |
isAllDay | The event is all-day. |
isMultiDay | The event covers more than one date, before clipping. |
continuesBefore, continuesAfter | The event extends past the range. |
Stacking follows event order. Display spans that only touch don't overlap.
Background events and layers are fractions of the track and take no stack rows. Timed shading clips to visible anchor
windows and can split at dates or DST gaps; all-day shading follows anchor dates, including their overnight portions.
Hidden weekdays get no shading. Layer ranges merge before inverse gaps are computed, and range callbacks receive
inputRange. In a resource timeline, unassigned background events shade every row, and assigned ones and
resource-limited layers shade only matching rows. An ungrouped timeline shows every background event.
Parts
| Part | Model prop | Element | Notes |
|---|---|---|---|
TimelineView | model | div | role='region', labelled "Timeline". Scrolls both ways. --cal-slot-columns sizes header slots. |
TimelineViewHeader | none | div | Sticky; holds the day headers and the slot labels. |
TimelineViewDayHeader | day | div | data-today, data-date, data-weekday. |
TimelineViewDayTrigger | calendar, day, view | button | Navigates through calendar to view on the day. See day triggers. |
TimelineViewSlotLabel | slot | div | slot is the time slot, not the HTML attribute. |
TimelineViewBody | none | div | Holds the rows and the now indicator. |
TimelineViewRow | row | div | data-unavailable, data-resource-id. |
TimelineViewResource | row | div | The resource cell; sticks to the start edge. |
TimelineViewResourceToggle | calendar, row | button | Expands/collapses descendants; aria-expanded, default label; omitted for leaves. |
TimelineViewLane | row | div | Holds the row's unavailable time and events. |
TimelineViewUnavailable | segment | div | aria-hidden. |
TimelineViewBackground | background | div | aria-hidden, data-layer, data-kind; render inside TimelineViewLane. |
TimelineViewBackgroundEvent | segment | div | aria-hidden, continuation flags; render inside TimelineViewLane. |
TimelineViewEvent | segment | button | data-all-day, data-multi-day, data-continues-before, data-continues-after. |
TimelineViewNowIndicator | model | div | aria-hidden. Renders only while now is inside the range. |
The parts render no slot cells; themes draw the slot lines as a background of each lane.
Rows expose data-depth, data-parent-id and data-expanded. Resource cells expose data-depth,
data-expanded and --cal-resource-depth. Themes indent each level by --calendar-resource-indent (1rem by
default); override that token to change the spacing. Toggle icons inherit the theme's text color and focus style.
With one slot per day, omit the slot-label row; the date header is enough. For subday slots, include minutes in the
labels. The examples put the full event dates and times in title and aria-label, so clipped bar text stays
available.