Skip to content

Gantt / resource timeline ​

Documentation · Integration · Calendar

Last verified: 2026-09-06

This is a separate component, not a CalendarView. V1 is resource scheduling: rows represent people, properties or other resources and bars represent assigned time intervals. Dependencies, milestones and task hierarchies are not implemented.

Native Swift baseline: see the iOS Timeline guide. The options below describe the Web implementation.

Web now supports consumer-confirmed pointer drawing and movement, optional synchronous data and custom entry/resource/group content. See the extensions below; the central feature catalog continues to track remaining parity work.

Contents ​

Integration and entry points ​

ts
import { createTimeline } from '@xsigns/chronosketch/timeline'
import '@xsigns/chronosketch/styles.css'
import '@xsigns/chronosketch/timeline/styles.css'

React exports Timeline from @xsigns/chronosketch/react and accepts individual TimelineOptions props. Vue exports Timeline from @xsigns/chronosketch/vue and accepts one options prop. All adapters use the same timeline composition and renderer. Timeline types are exported through @xsigns/chronosketch/timeline.

Data contracts ​

  • TimelineResource: id, label, optional avatarUrl and groups (string-keyed values).
  • TimelineEntry: id, title, start, end. Intervals use floating YYYY-MM-DDTHH:mm minutes and exclusive ends, without timezone conversion.
  • TimelineAssignment: id, entryId, resourceId. One entry can have several assignments.
  • TimelineData: readonly entries and assignments arrays.
  • TimelineGrouping: key (resource.groups key) and label.
  • TimelineRange: start and end in the same floating minute format.
  • TimelineQuery: range plus resourceIds for the buffered visible rows.

Resource, entry and assignment IDs must be non-empty and unique within their collections. Assignments must reference existing supplied resources and entries in the same response. Returned data is validated, copied and frozen. Resource group values are data, not arbitrary executable grouping functions.

Group paths are projected from the ordered groupBy definitions. Missing values appear under a localized Unassigned label. Expanding/collapsing changes the visible row projection without changing the resource or event data. Groups themselves cannot receive assignments. Changing groupBy resets collapse state.

Complete Vanilla example ​

ts
import { createTimeline } from '@xsigns/chronosketch/timeline'
import type { TimelineData, TimelineOptions } from '@xsigns/chronosketch/timeline'
import '@xsigns/chronosketch/styles.css'
import '@xsigns/chronosketch/timeline/styles.css'

const allData: TimelineData = {
    entries: [
        { id: 'booking', title: 'Workshop', start: '2026-09-06T09:00', end: '2026-09-06T17:00' },
    ],
    assignments: [
        { id: 'booking-room', entryId: 'booking', resourceId: 'room' },
        { id: 'booking-person', entryId: 'booking', resourceId: 'person' },
    ],
}
const options: TimelineOptions = {
    range: { start: '2026-09-01T00:00', end: '2026-10-01T00:00' },
    date: '2026-09-06',
    scale: 'day',
    resources: [
        { id: 'room', label: 'Meeting room', groups: { site: 'Berlin' } },
        { id: 'person', label: 'Alex', groups: { site: 'Berlin' } },
    ],
    groupBy: [{ key: 'site', label: 'Location' }],
    loadData: async (query, signal) => {
        signal.throwIfAborted()
        const entries = allData.entries.filter(entry => entry.start < query.end && entry.end > query.start)
        const ids = new Set(entries.map(entry => entry.id))
        const assignments = allData.assignments.filter(assignment =>
            query.resourceIds.includes(assignment.resourceId) && ids.has(assignment.entryId))
        return { entries, assignments }
    },
    onEntryClick: ({ entry, assignment }) => console.log(entry, assignment),
}

const host = document.querySelector<HTMLElement>('#timeline')
if (!host) throw new Error('Missing timeline host')
const timeline = createTimeline(options)
timeline.mount(host)
// timeline.destroy() when the host is removed.

Use Timeline with {...options} in React or :options="options" in Vue. Your product owns images, labels, persistence and backend resource models.

Options and methods ​

OptionDefault / meaning
width, height, minWidth, maxWidth, minHeight, maxHeightOptional number (CSS pixels) or CSS string; see Component sizing
resourceTitleOptional left column heading; defaults to Ressourcen / Resources
titleOptional custom heading; defaults to Ressourcenplanung / Resource planning
resourcesComplete resource collection, or [] when loadResources owns cursor pages
rangeRequired finite horizon: { start, days } (end-based input is deprecated); see below
loadData(query, signal)Optional Promise<TimelineData> provider; omit for synchronous data
loadingOptional TimelineLoadingPolicy; buffered requests and bounded window cache
dateInitial YYYY-MM-DD (midnight) or exact floating YYYY-MM-DDTHH:mm anchor; defaults to range start
showPeriodHeadertrue; visual month/week/day overview above the time ticks
showZoomtrue; show the time zoom slider without changing the zoom level
zoom1; 1, 2, 4, 6 or 8 scale units per fixed-width column
scalehour, day or week; default day
groupIconsOptional icon mapping by grouping key and raw value; see below
groupByOrdered grouping definitions; default empty
localede-DE; German/English controls and Intl time labels
controlsStylerounded; squared/flat use the shared presets
iconSetmaterial; font-awesome also supported
allowOverlapDefaults to false; explicitly enable overlapping draw/move proposals
onEntryCreateRequestedEnables drawing; receives resourceId, start and exclusive end
onEntryEditRequestedEnables moving; receives previous/proposed entry and assignment
entryActionsOptional TimelineEntryAction array or synchronous selection resolver
onEntryContextMenuOptional secondary-activation callback receiving
onEntryClickOptional ({ entry, assignment }) => void

Methods: mount, destroy, getSnapshot, subscribe, setResources, setGroupBy, setGroupIcons, setResourceTitle, setGroupsExpanded, setLoadingPolicy, setScale, setZoom, setShowZoom, setDate, setSize, setTitle, setShowPeriodHeader, setEntryActions, setEntryContextMenuHandler and reload. setSize replaces all six sizing fields; no argument resets them. getSnapshot includes resources, groupBy, scale, zoom, data and the current row projection. It does not include pixel scroll position. subscribe returns an unsubscribe function. Invalid state inputs throw before replacing confirmed state. setResources clears previously loaded data and requests the current viewport again; reload retries the last query explicitly.

React/Vue synchronize resources, groupBy, scale, zoom, date, sizing and loadData updates. Changes to range, locale, controlsStyle or iconSet recreate their instance. Keep these inputs stable when preserving scroll/collapse state matters. Vanilla recreates for those construction options. destroy cancels loading, subscriptions and ResizeObserver/frame callbacks; re-mounting is supported. Imports and factory construction are DOM-free; mounting needs a browser.

Viewport and provider lifetime ​

A single scroll owner drives header, labels and bars. The sidebar is sticky; the time header is sticky vertically. Render only the visible row slice plus three leading rows and trailing buffer, and visible time ticks plus a time-step buffer. Hour/day/week correspond to 60/1440/10080 floating minutes per tick at zoom 1; zoom multiplies those durations. Ticks are anchored to range.start; week scale is seven-day blocks, not ISO weeks.

Queries include the buffered time interval and buffered resource IDs. Providers must return overlapping intervals, including entries starting before query.start. There is no backend URL, GraphQL client or product cache in the library.

Configurable buffering and cache ​

ts
loading: {
    prefetchViewports: 2,
    minPrefetchMinutes: 30 * 24 * 60,
    triggerViewports: 0.5,
    minTriggerMinutes: 7 * 24 * 60,
    debounceMs: 80,
    maxCachedWindows: 8,
}

These values are an example, not product defaults. Each side receives the larger of viewport duration times prefetchViewports and minPrefetchMinutes, clipped to the horizon. The trigger uses the corresponding trigger values: start loading when that expanded viewport is not covered, while already known content stays visible. Viewport duration includes the existing one-column render buffer.

Defaults are zero for both prefetch/trigger pairs, 80 ms debounce and eight cached windows. Numbers must be finite and nonnegative; cache size must be a positive integer. Each trigger value must not exceed its corresponding prefetch value. setLoadingPolicy and React/Vue option changes apply live and invalidate the cache.

Successful responses, including empty results, contribute resource/time coverage. Returning to a retained window or zooming within it does not fetch or show skeletons. Partly unknown rows show skeletons only in uncovered segments. The latest response replaces overlapping assignments for its requested rectangle; other windows remain. The oldest response is evicted at the configured limit. Data outside retained coverage is not claimed as loaded. Empty resource queries never call the provider. Resources still arrive as a complete input collection.

Requests are debounced and superseded work receives an aborted signal. Generation checks discard late responses from providers that ignore cancellation. Errors show a localized retry control without automatically retrying the same query. reload, provider/resource replacement and destruction invalidate the cache. Cache replay is a snapshot, not a freshness guarantee; invalidate on global data changes. Lazy setData updates the most recently accepted response window, useful for that window's subscription updates. Resolve each new request through its provider promise before sending subscription updates through setData. Entry renderers/callbacks must use the supplied entry, including retained custom display fields, rather than looking it up solely in the latest provider response.

Styling and current limits ​

Load both shared styles.css and timeline/styles.css. The timeline root carries xc-calendar for shared theme roles and xc-timeline for timeline-specific hooks. Existing light/dark palettes and outer shadow/radius variables apply. --xc-timeline-sidebar defaults to 240px (150px on narrow screens); use px units. Resource rows start at 60px; collapsible group rows are 40px at every depth. Virtualization uses fixed resource-row heights (60px on Web, 64pt on iOS) and cumulative offsets. One assignment lane remains visible; N weitere / N more opens the other overlapping assignments without changing row height. Adjacent exclusive-end intervals reuse the visible lane.

Public hooks: xc-timeline, xc-timeline-viewport, xc-timeline-header, xc-timeline-tick, xc-timeline-row, xc-timeline-group, xc-timeline-label, xc-timeline-avatar, xc-timeline-entry and xc-timeline-status. Avatar URLs belong to the consumer; missing images hide gracefully. Labels and titles render as text. Timeline locale overrides and per-slot icon overrides are not yet part of this separate options contract.

V1 supports entry clicks/context actions, viewing, grouping, scrolling and lazy entry loading. Draw, touch drag, task dependencies, milestones, infinite time-horizon extension remains a future slice. Web resource pagination is described below; native providers are documented for Swift and Kotlin. The viewport is a keyboard-scrollable region with native group/event buttons, not a complete ARIA treegrid or certified accessibility implementation.

Entry clicks and context actions ​

onEntryClick({ entry, assignment }) handles primary clicks and native button keyboard activation. onEntryContextMenu handles secondary activation: right-click, ContextMenu/Shift+F10 or a 550ms touch hold. It is separate from the primary callback. Both receive the full readonly stored entry and the specific assignment. The latter contains resourceId, so entries assigned to multiple resources remain unambiguous. Resource labels, empty cells and group headers do not emit entry callbacks.

ts
import type { TimelineOptions } from '@xsigns/chronosketch/timeline'

const interactions = {
    onEntryClick: ({ entry, assignment }) => console.log('Click', entry, assignment),
    onEntryContextMenu: ({ entry, assignment }) => console.log('Context', entry, assignment),
    entryActions: selection => [
        {
            id: 'details',
            label: 'Show details',
            onSelect: ({ entry, assignment }) => console.log('Details', entry, assignment),
        },
        {
            id: 'edit',
            label: 'Edit',
            disabled: selection.entry.id === 'locked',
            onSelect: ({ entry }) => console.log('Edit', entry),
        },
    ],
} satisfies Pick<TimelineOptions, 'onEntryClick' | 'onEntryContextMenu' | 'entryActions'>
// Spread interactions into your TimelineOptions alongside resources/range/loadData.

entryActions accepts an array or a synchronous per-opening resolver. Each action has id, label, optional disabled and onSelect(selection). Use unique IDs and consumer-localized labels. The built-in Material menu closes before onSelect runs; no data is changed or persisted automatically. Return no actions to omit the menu. If neither actions nor a context callback handle activation, the browser's native context menu remains available. A configured context callback suppresses it even without actions; it receives semantic data, not pointer coordinates or a DOM event. The action resolver runs before the context callback. Touch holds suppress the following primary click and require a fresh interaction to select a menu item.

setEntryActions(value) replaces the action configuration; omission removes it. setEntryContextMenuHandler(callback) replaces the secondary callback; omission removes it. Both cancel open menus/pending holds. React/Vue synchronize these options and forward current primary callbacks. Menus also close on scroll, Escape, outside interaction, viewport/data refresh and destruction, so virtualized rows cannot retain stale selections. No native iOS/Android adapter is implied.

Visual time overview ​

showPeriodHeader defaults to true. Set it to false to hide the compact 32px row above Resources and the time ticks, or call setShowPeriodHeader(false) later. React/Vue synchronize the option. The demo's Zeitübersicht toggle persists it in its URL and preserves resource grouping, collapse state and scroll position.

Time scaleOverview sections
weekCalendar months, localized name and year
dayISO calendar weeks, Monday start, week number and ISO week-year
hourCivil days, localized date

This is a presentation aid, with no grouping/filtering/data mutation behavior. Month/day/ISO-week boundaries use floating civil time and exclusive ends; the weekly tick grid still uses seven-day blocks anchored at range.start. Consequently, month separators can fall inside a weekly tick. Only overview section boundaries have vertical borders; lower tick borders do not extend into this row.

Both header rows stay visible on vertical scroll. On horizontal scroll each label sticks beside the resource column until its section's trailing boundary pushes it out, then the next section takes over. Long labels are truncated inside their own section, with their full text in title. Sections are rendered only for the buffered visible time range, but retain full period bounds clipped to the finite horizon.

Custom title ​

Set title: 'Buchungsübersicht' in TimelineOptions (Vanilla/Vue) or pass the title prop to React's Timeline. The product owns translation of its custom text. Omission uses the localized Ressourcenplanung / Resource planning default. Vanilla can call timeline.setTitle('Steuerungsübersicht'); setTitle() restores the localized default. React/Vue synchronize title updates without remounting or replacing controls. The heading survives time-scale changes and remounting. Text is rendered literally, not as HTML. An empty string leaves the visible title empty; the component retains its localized accessible name in that case. The title is presentation state and does not notify application subscribers.

Group titles and icons ​

Visible group titles come from the values in resource.groups: with groups: { type: 'Properties' }, the group is titled Properties. resource.label names the resource row. groupBy.key selects the resource field; groupBy.label describes that grouping dimension and is currently not displayed. The consumer owns these texts and their translation. Missing/empty values show localized Unassigned text.

Optional groupIcons maps a grouping key and its raw value to an included icon identifier. The same value under another key is independent.

ts
groupBy: [{ key: 'type', label: 'Resource type' }],
groupIcons: {
    type: {
        Properties: 'material:apartment',
        Team: 'material:groups',
    },
},

Icons are decorative inline SVGs beside the title; unconfigured groups have no icon. Use the empty string key for Unassigned. Identifiers include their icon-set prefix and are independent of the toolbar's iconSet. HTML and remote SVG URLs are not accepted. Unknown identifiers throw before replacing active settings. Configuration is copied and frozen; replace it to update icons.

Vanilla supports timeline.setGroupIcons(mapping); setGroupIcons() removes all group icons. React synchronizes the groupIcons prop; Vue synchronizes options.groupIcons. Updates preserve grouping, collapse state and scroll; icons are presentation settings, excluded from store snapshots/subscriptions.

Resource column title ​

resourceTitle: 'Tiere' customizes the top-left column heading independently of title (the overall timeline heading). Vanilla supports timeline.setResourceTitle('Tiere'); omission restores the localized default. React synchronizes resourceTitle, Vue synchronizes options.resourceTitle. Updates preserve the instance, scroll and collapsed groups. Consumer text renders literally, with long headings truncated and the full text available in title. An empty string intentionally leaves the heading empty.

The icon-only button beside the resource column title collapses all groups when any group is open; when all are collapsed it expands every level. It affects the complete resource tree, including offscreen groups. Tooltip and accessible name are localized (German/English); Enter and Space work with focus preserved. No button is shown without groups. Its SVG follows iconSet. Vanilla can also call timeline.setGroupsExpanded(false) to collapse all or timeline.setGroupsExpanded(true) to expand all. Each command notifies once.

Time-axis zoom ​

Set showZoom: false to hide the slider; it defaults to true. The configured zoom and setZoom() still work while hidden. Vanilla can call timeline.setShowZoom(false) and timeline.setShowZoom() restores visibility. React synchronizes showZoom, Vue synchronizes options.showZoom. Toggling it preserves zoom, data and the mounted instance. If the slider holds keyboard focus when hidden programmatically, focus moves to the timeline viewport. The playground's Zeitzoom setting also toggles visibility and persists in its URL.

The toolbar slider changes how many time units one fixed-width column contains. zoom accepts 1 (default), 2, 4, 6 or 8. For example, scale: 'day', zoom: 4 shows four days per column. Hour/week aggregate hours/weeks respectively. Text, icons and resource rows keep their size; entry start/end values stay exact. The period overview retains actual day/week/month boundaries.

ts
const timeline = createTimeline({ ...options, scale: 'day', zoom: 2 })
timeline.setZoom(4)
timeline.setZoom() // restore one unit per column

React synchronizes the zoom prop and Vue synchronizes options.zoom; removing it restores 1. User slider input and setters update getSnapshot().zoom and notify subscribers. Invalid zoom values throw before changing confirmed state. Zoom is retained across time-scale changes and remounts. React/Vue prop semantics match scale: an unchanged prop does not overwrite subsequent slider input.

Moving the slider right zooms in (fewer units per column), left zooms out. Arrow keys, Home and End use native range-input behavior, with localized accessible values. Changes keep the time at the viewport center fixed where the finite horizon permits. Near either edge, scroll clamps to that horizon; when the whole horizon fits, additional empty space can remain. The range never grows automatically. Scale changes now preserve the center by the same rule.

The slider remains enabled during loading. Requests are coalesced for 80ms and superseded responses cannot replace newer data or loading status. Previous bars remain displayed; hatched regions indicate time/resources not covered by the last successful response, including after a failed request. This is not an availability indicator. The viewport exposes aria-busy while loading; status and retry explain failures. There is no accumulating data cache.

Resource row heights stay fixed while loading and after successful replacement. Changing the visible time range updates the overflow counts and available entries without moving later rows. Grouping and period-header changes can still alter the positions of resource rows.

Entry avatars and icons ​

entryPresentation: entry => ({ image: { src: '/guests/ada.jpg' }, icon: 'material:check' }) adds decorative media to entry bars. Resolve guest data by entry ID. Images default to round avatars; image.variant: 'logo' contains the full logo. Failed images fall back to icon, or disappear. Keep meaningful guest names in the entry title. The callback returns EventMedia; EventMedia and EventImage are exported from @xsigns/chronosketch/timeline. setEntryPresentation(callback) refreshes visible bars; calling it without a value restores plain titles. React/Vue synchronize changes.

Horizontal horizon ​

The scroll horizon is independent of entry count and loaded windows. To show 1095 days even with one booking (or no bookings), use:

ts
range: { start: '2026-09-07T00:00', days: 1095 }

days must be a positive integer and measures floating 24-hour calendar days from start, preserving its time of day. The computed end is exclusive. Specify exactly one of days and end. TimelineHorizon is the exported options type; provider queries still use resolved TimelineRange values with start/end.

Legacy compatibility only: an existing range.end remains accepted but is deprecated. New integrations compute a positive whole-day length and supply range.days:

ts
range: { start: '2026-09-07T00:00', end: lastEventEnd }

Obtain lastEventEnd from the complete dataset or backend metadata. The component does not infer it from lazily loaded entries, which cannot reveal unseen future bookings. With no events, supply a fixed fallback horizon. Events beyond the configured horizon do not extend it. Zoom and lazy loading retain the same bounds.

React/Vue recreate their instance when start, end or days changes, as with the existing range contract. Vanilla callers recreate for a different horizon.

Selection and precise consumer editing ​

Available since 1.0.0-beta.14: drawing and movement are available in Vanilla, React and Vue, matching the native iOS interaction rules. Click/tap and context actions still select original values. End resizing is not enabled.

Provide onEntryCreateRequested to enable drawing on an empty resource row. Its request contains resourceId, start and exclusive end; the consumer supplies identity/title and confirms through setData, React data or Vue options.data. Provide onEntryEditRequested to enable dragging existing bars. Its immutable request contains previous, proposed and the initiating assignment. Movement preserves exact duration, title, identity and all resource assignments.

ts
const timeline = createTimeline({
    resources, range, data,
    allowOverlap: false, // default; explicitly set true to permit collisions
    onEntryCreateRequested: request => {
        // Open your form, assign IDs/title, persist, then timeline.setData(nextData).
    },
    onEntryEditRequested: request => {
        // Validate/persist before confirmation in a real integration.
        data = applyTimelineEntryEdit(data, resources, request)
        timeline.setData(data)
    },
})

allowOverlap defaults to false. A proposal cannot intersect another entry on any assigned resource; adjacent exclusive-end intervals are valid. Moving an entry excludes that same entry from collision detection. Hidden overflow entries and collapsed resource assignments participate. Imported data can still contain existing overlaps; the option governs interactive requests, not data ingestion. timelineHasOverlap(data, range, resourceIds, excludingEntryId?) exposes the same pure rule for consumer forms. Programmatic applyTimelineEntryEdit validates identity/freshness but does not enforce interactive overlap policy by itself.

Hour view snaps drawing to 15-minute cells; Day/Week use whole civil days regardless of zoom. Movement applies these increments while retaining original off-grid times. Drag either direction to draw; a minimum pointer movement avoids accidental creation. Invalid previews are marked red and emit no request. Escape, pointer cancellation, scrolling, data/configuration replacement and teardown clear Web previews. No confirmed data changes until the consumer supplies its updated snapshot.

Replace settings/callbacks together with timeline.setInteractions({ allowOverlap, onEntryCreateRequested, onEntryEditRequested }). Omitted callbacks disable their gestures; omitted allowOverlap restores false. React/Vue synchronize these options without remounting.

With a provider, previews require successfully loaded coverage for the proposed interval on every known assigned resource. Unknown time is not treated as free. The provider must return the complete relevant assignments of shared entries. The consumer must check authoritative data before saving, persist edits before confirming, and return them in future queries. Confirmation cancels older reads. There is no automatic scrolling during a gesture or resource reassignment. Keyboard users can open entry details/context actions for a precise consumer form.

The development demos confirm requests locally and expose an Allow overlaps switch. Default fixtures have four resources and three 1/2/3-day entries beginning at midnight. ?timelineStress=1 enables dense loading/overlap fixtures.

Custom resource, group and entry content ​

entryPresentation(entry) may now also return color and textColor, alongside icon and image. Values are CSS colors; native Swift uses its own Color type.

Vanilla accepts entryRenderer(context, host), resourceRenderer(context, host) and groupRenderer(context, host). Return a disposer for owned listeners or framework content; it runs before replacement and teardown. Replace decorative content only; keep controls out of the library-owned entry/group button. Entry context contains original entry, assignment, resource and availableWidth. Resource context contains resource; group context contains key, raw value, structured path, localized fallback title, count and expanded. Group disclosure/count remain outside custom title content.

React supplies renderEntry, renderResource and renderGroup props returning React nodes. Vue supplies entry, resource and group scoped slots. Both retain the common DOM ownership and dispose portals/teleports on viewport replacement. Vanilla can replace the renderer collection with setContentRenderers({...}).

setDate('2026-09-09T09:30') navigates to an exact floating minute on Web, matching native LocalDateTime navigation. Civil dates still select midnight. Invalid anchors throw before changing state; viewport movement is bounded by the configured horizon. React date and Vue options.date accept the same forms.

Canonical horizon configuration is start plus a positive whole-day count on Web and iOS. Both Gantt demos use days: 90; zoom changes density, not these bounds. The legacy end branch remains source compatible during migration. Do not supply both fields. Calendar navigation limits are unrelated and unchanged.

Height modes ​

Resource Timeline and TaskGantt accept heightMode: 'fill' | 'content' (default 'fill'). Fill retains the allocated height; unused space below the final row has no column grid lines. Content ends after the visible rows, toolbar/header and status area. Adding/removing rows and group disclosure update its height; resource overlap counts do not. Grid lines end at the last actual row in both modes.

ts
heightMode: 'content',
maxHeight: 600,

In content mode height, when supplied, also acts as an upper bound; with both height and maxHeight the smaller limit applies. minHeight retains normal CSS precedence and may intentionally leave blank space. Without a height limit, content grows to fit all rows. Fill keeps the existing 520px/70vh viewport default when no size is supplied. Constrained content scrolls internally; controls and pinned headers remain available. Percentage limits need a definite parent height.

Both instances expose setHeightMode(mode = 'fill'). React props and Vue options synchronize this setting without remounting, preserving collapse and confirmed data. setOptions on TaskGantt still performs its documented full replacement.

Horizontal overflow exposes a visible scrollbar below the viewport, aligned with the time columns. Drag its thumb, click its track or focus it and use Left/Right, PageUp/PageDown, Home/End. Trackpad and native scrolling remain available. See scrollbar styling.

Content and header density ​

density and headerDensity independently accept comfortable (default) or compact. Configure options/props or call setDensity / setHeaderDensity. See the shared density contract for geometry, live updates and demo controls. Available since beta.20.

refresh() redraws the mounted viewport from its current snapshot without replacing data, cancelling a provider request or marking unknown ranges loaded. It supports external row decoration updates such as Resources constraints.

Vertical resource pagination (Web) ​

loadResources(query, signal) returns { resources, nextCursor? }. Omit nextCursor on the terminal page. Pass resources: [] and keep loadData for entries: it receives the buffered time range and IDs of loaded rows. The callbacks are independent. Horizontal navigation and zoom do not fetch more resource pages. Vanilla, React and Vue use the same implementation; Resources with layout: 'timeline' accepts the same options.

ts
resources: [],
height: 500,
resourceLoading: {
    minimumRows: 1,
    prefetchViewports: 0,
    minPrefetchRows: 2,
    triggerViewports: 0,
    minTriggerRows: 1,
},
loadResources: async ({ cursor, limit }, signal) => fetchResourcePage({ cursor, limit, signal }),

minimumRows is a lower bound, never a viewport limit. The effective request fills the measured scroll viewport (excluding sticky headings), plus the larger of prefetchViewports times its row height in pixels or minPrefetchRows rows. The default is 20 minimum rows, one viewport or ten prefetch rows, and half a viewport or five trigger rows. Explicit trigger values cannot exceed prefetch values; omitted triggers are capped to the configured buffer. A provider may return smaller nonempty pages when the server enforces its own page limit; ChronoSketch continues until the measured target is filled or the source ends. Resize and density changes recalculate demand. Group headings count by their actual layout height, not as resource rows.

Paged loading uses heightMode: 'fill', even when content was supplied. height therefore bounds the component frame rather than a growing row canvas. Without an explicit size, the finite CSS viewport default applies. A hidden or zero-height row viewport waits for layout instead of loading the entire dataset.

Pages are appended and retained while scrolling upward. Grouping/collapse and scroll anchors remain intact. A page adding no visible height pauses automatic continuation until viewport geometry or grouping changes; collapsed groups must not drain the server. Providers should return a stable order aligned with groups. Group counts represent loaded resources, not an unknown server total.

Requests are serial and abortable. Duplicate IDs, repeated cursors and empty nonterminal pages show a retryable error without advancing the cursor. A failed page retains previous rows and entries; explicit retry repeats that cursor. The bottom-centered loading indicator does not change layout height. Empty terminal pages are valid and stop further loads. reloadResources() aborts and resets rows and entry coverage. Provider replacement in React/Vue resets the resource scope; replace the callback for changed filters or tenant scope. setResourceLoadingPolicy updates thresholds without discarding loaded rows. setResources patches metadata of already loaded IDs while the cursor provider owns row membership; use reloadResources to change membership or filters.

The new capability is Web-only in this incremental slice. Swift cursor pagination is tracked as TL-014 catch-up; existing Swift horizontal buffering is unchanged.

ChronoSketch · Calendar and scheduling components