spectime
v0.1.1
Published
Spectime timeline components built on chronon-timeline.
Maintainers
Readme
spectime
spectime is an opinionated UI layer on top of my library chronon-timeline.
It doesn’t reimplement the timeline engine — you still use chronon-timeline for the core primitives (Timeline, Row, Subrow, Item), the data model (Range, RowDefinition, ItemDefinition), and state helpers like useTimeline() (re-exported as useSpectimeTimeline() from spectime). spectime adds styling, convenience wrappers, and a small hook for turning a flat list of items into subrows (lanes).
Composition pattern (how you build UIs with it)
chronon-timeline is built around the composition pattern: instead of a single “mega timeline component”, you compose a timeline from small primitives:
Timeline(shell + context)TimeAxis(markers/labels)Row(one row in the timeline, with optional sidebar)Subrow(a lane inside a row)Item(a span in time)
spectime keeps that same model: you compose your own layout, but with pre-styled wrappers and a helper hook for lane packing.
Mentally, it looks like:
SpectimeTimelineProvider (context + interactions)
SpectimeTimelineContainer (shell)
SpectimeTimeAxis (headers)
SpectimeRow[] (rows)
SpectimeSubrow[] (lanes)
SpectimeItem[] (items)Installation
spectime depends on chronon-timeline and expects these peer deps to be installed in your app:
npm i spectime chronon-timeline react react-dom react-virtuosoBuilding blocks (what each exported piece does)
Core wrappers:
SpectimeTimelineProvider: wrapsTimelineContextProviderand enables mouse pan/zoom + “autopan until user interaction”; exposesuseSpectimeTimelineContext()forresetAutopan().SpectimeTimelineContainer: wrapsTimelineand applies the Spectime shell styling (and default RTL direction unless you override).SpectimeTimeAxis: renders the main time axis (plus the smaller hour axis + optional highlight row) using Spectime’s marker definitions.SpectimeRow: wrapschronon-timeline’sVirtualizedRowand turns onvirtualizeSubrows(subrow virtualization) for better performance with many lanes.SpectimeSubrow: a styled wrapper aroundSubrow.SpectimeItem: a styled wrapper aroundItemwith an inner container for your custom content.
Common add-ons:
SpectimeSidebar: a styled sidebar renderer you can pass assidebarto a row.SpectimeTimeCursor/SpectimeCurrentTimeCursor: cursor components for rendering “now” or an arbitrary timestamp.
Helpers/hooks:
useSpectimeTimeline: re-export ofchronon-timeline’suseTimeline()hook (range + zoom state).useVisibleTimelineItems: groups items into non-overlapping lanes (subrows) for the current visible range (must be used insideSpectimeTimelineProvider).currentTimeAtom: a Jotai atom that ticksDate.now()every second (used bySpectimeCurrentTimeCursorand autopan).
Basic usage
import { useState } from 'react';
import type { ItemDefinition, Range, RowDefinition } from 'chronon-timeline';
import {
SpectimeItem,
SpectimeRow,
SpectimeSubrow,
SpectimeTimeAxis,
SpectimeTimelineContainer,
SpectimeTimelineProvider,
currentTimeAtom,
useSpectimeTimeline,
useVisibleTimelineItems,
} from 'spectime';
export const MyTimeline = ({
rows,
items,
}: {
rows: RowDefinition[];
items: ItemDefinition[];
}) => {
const [range, setRange] = useState<Range>(() => {
const now = Date.now();
return { start: now - 60 * 60 * 1000, end: now + 60 * 60 * 1000 };
});
const timeline = useSpectimeTimeline({ range, setRange });
return (
<SpectimeTimelineProvider {...timeline} currentTimeAtom={currentTimeAtom}>
<TimelineContents rows={rows} items={items} />
</SpectimeTimelineProvider>
);
};
const TimelineContents = ({
rows,
items,
}: {
rows: RowDefinition[];
items: ItemDefinition[];
}) => {
const SUBROW_HEIGHT_PX = 60;
const { subrowsByRow, rowIdWithMostVisibleLanes, isWeekly } = useVisibleTimelineItems({
rows,
items,
});
return (
<SpectimeTimelineContainer>
<SpectimeTimeAxis isWeekly={isWeekly} />
{rows.map((row) => (
<SpectimeRow
key={row.id}
{...row}
subrowHeight={SUBROW_HEIGHT_PX}
ignoreRefs={row.id !== rowIdWithMostVisibleLanes}
>
{(subrowsByRow[row.id] ?? []).map((lane, laneIndex) => (
<SpectimeSubrow key={`${row.id}-${laneIndex}`}>
{lane.map((item) => (
<SpectimeItem key={item.id} id={item.id} span={item.span}>
{String(item.id)}
</SpectimeItem>
))}
</SpectimeSubrow>
))}
</SpectimeRow>
))}
</SpectimeTimelineContainer>
);
};Notes:
SpectimeRowwrapschronon-timeline’sVirtualizedRowand enablesvirtualizeSubrowsby default.- This package ships styles bundled into the JS build (no separate CSS import).
Subrows (lanes) and useVisibleTimelineItems
In chronon-timeline, each Row can contain multiple Subrows. A subrow is effectively a “lane” used to display items that would otherwise overlap in time.
useVisibleTimelineItems({ rows, items }) is a convenience hook that:
- Must be called from a component rendered inside
SpectimeTimelineProvider(it reads the currentrangefrom timeline context). - Filters items to what intersects the current timeline range.
- Groups items by
rowIdand packs them into the minimum number of non-overlapping lanes (subrows) so items in the same lane never overlap in time. - Exposes
isWeekly, a boolean derived from the current visible range size, which is typically used to switch time-axis marker density and to optionally expand item spans to full-day blocks when zoomed out.
Return shape:
subrowsByRow:Record<string, ItemDefinition[][]>(each inner array is one lane/subrow)rowIdWithMostVisibleLanes: a helper for theignoreRefsoptimization patternisWeekly: whether the visible range is “zoomed out” (currently ~3+ days)
Why a fixed subrowHeight helps performance
subrowHeight is intentionally a required input, because it removes a whole class of expensive layout work:
- No DOM measuring for lane sizing. When every subrow has a known fixed height, the row layout can be expressed via a single CSS variable (
--tl-subrow-height) instead of repeatedly measuring content. - Virtualization becomes predictable.
SpectimeRowusesVirtualizedRow+react-virtuosofor subrow virtualization. A stablesubrowHeightlets the virtualizer compute scroll offsets and overscan purely from math (defaultItemHeight), instead of doing extra work to guess/measure heights. - Less churn when lots of subrows exist. If you have many lanes, keeping their height fixed prevents “useless recalculations” (reflow/relayout passes triggered by variable heights) and makes scrolling/panning smoother.
Practical tip: treat subrowHeight as a constant per zoom mode (e.g. 60 for dense view, 40 for weekly), and avoid dynamically changing it per lane.
Provider helpers
SpectimeTimelineProvider is a thin wrapper around chronon-timeline’s TimelineContextProvider that also enables:
- mouse pan + zoom
- auto-pan that keeps “now” moving until the user interacts
If you need access to timeline context plus resetAutopan(), use useSpectimeTimelineContext(). This is useful when you jump the range programmatically (e.g. a “preset range” toolbar) and want auto-pan to resume.
Local demo (this repo)
This repo contains dev-only examples under src/examples/* (not shipped in the npm package):
npm install
npm run dev