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 mouse/keyboard interval editing, 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
resourcesRequired complete resource collection; an empty array is allowed
rangeRequired finite horizon: { start, days } (end-based input is deprecated); see below
loadData(query, signal)Required Promise<TimelineData> provider
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
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, 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.

Scroll requests are coalesced for 80 ms, consecutive identical queries are skipped, and superseded requests receive an aborted signal. Generation checks also discard late responses from providers that ignore cancellation. No resources means no provider call. Errors show a localized retry control; previous confirmed data remains until a successful response replaces it. Retry and provider replacement can explicitly reload an otherwise identical range.

Only the current returned window is held; there is no accumulating range cache. Returning to a previous window can refetch. Resources are currently loaded as a complete input collection; server-paginated resource trees are a future contract.

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 cumulative row offsets for both heights. All overlapping assignments receive visible lanes; rows grow to fit them without an overflow cap. Adjacent exclusive-end intervals reuse a 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, resource pagination and native adapters are future slices. 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.

Measured row heights stay stable while waiting for data. On a successful replacement, overlap lanes and heights update together and resource anchoring compensates for height changes above the viewport. A completed response may therefore change row heights; pixel-identical layouts across different date windows are not promised.

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

Gantt bars are selectable but cannot be moved or resized directly, with mouse, touch or keyboard. Click/tap opens the consumer's details or editor; existing context actions remain available. This matches iOS. onEntryEditRequested and setEntryEditHandler have been removed; remove these options from integrations. Calendar and TaskGantt editing are unaffected.

For a consumer form, editTimelineEntry and applyTimelineEntryEdit remain validated immutable helpers. Confirm through setData (Vanilla), data (React) or options.data (Vue). Shared assignments keep the same entry identity. With a provider, persist in its authoritative source before confirming: setData cancels older in-flight reads, but future queries must return confirmed values. loadData remains optional for synchronous data integrations.

The development demo has four resources and three entries: one, two and three full days. All entries begin/end at midnight, with no hidden half-day extension or shared assignment. ?timelineStress=1 opts into dense loading and overlap fixtures used by stress/layout tests.

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, resource overlap lanes and group disclosure update its height. 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.

ChronoSketch · Calendar and scheduling components