@trailvine/react

A <Timeline /> component that renders real JSX/SVG directly — no dangerouslySetInnerHTML, no dependency on @trailvine/vanilla — plus a useTimelineLayout hook for anyone who wants the raw geometry instead of the packaged component.

Both accept every field from the shared options reference (points, layout, path, wrap?, mobileBehavior?) — this page only covers what's added on top.

<Timeline />

function Timeline<T = {}>(props: TimelineProps<T>): JSX.Element;

interface TimelineProps<T = {}> extends TrailvineOptions<T> {
  className?: string;
  style?: CSSProperties;
  onPointHover?: (point: ExtendedTimelinePoint<T>, index: number) => void;
  caption?: string;
  captionPosition?: 'top' | 'bottom'; // default: 'top'
}
PropNotes
className / stylePassed straight through to the outer <svg>.
onPointHoverFires on both onMouseEnter and onFocus, so keyboard users trigger it too, not just mouse users.
captionOptional free-text label — e.g. "Sprint 14". Space for it is reserved on the canvas whether or not it's actually set, so adding/removing one never shifts the layout.
captionPosition'top' (centered, horizontal orientation) or 'bottom'. In 'vertical' orientation the caption is left-anchored instead.

Colors

The path and points aren't styled via props — they read two CSS custom properties, with sensible defaults, from whatever ancestor element sets them:

<div style={{ '--trailvine-path-color': '#4A5568', '--trailvine-point-color': '#D4A24C' }}>
  <Timeline points={points} layout={layout} path={path} />
</div>

Multi-instance safety

Every rendered <Timeline /> gets its own useId()-scoped class/id prefix, so two instances on the same page never collide — verified by an actual test rendering two as siblings and confirming they get distinct IDs, not just by code review.

useTimelineLayout

Returns raw computed geometry without rendering anything — for building your own markup around Trailvine's math.

function useTimelineLayout<T = {}>(options: TrailvineOptions<T>): TimelineLayoutResult;

interface TimelineLayoutResult {
  path: string | string[];
  points: Point[];
}

Memoized by serializing options to a string — it recomputes when the options' content changes, not just when the object reference changes. wrap.measureFn (a function) is dropped by that serialization, which is harmless here since this hook never calls wrapText itself.

Requirements

Peer dependency: react and react-dom >=18.0.0. This is a hard requirement, not just a recommendation — useId() (used for the multi-instance safety above) doesn't exist in React 17.

Next steps