@trailvine/vanilla

Plain JS/DOM binding — renderTimelineSVG for a pure, SSR-safe markup string, and mountTimeline for mounting (and cleanly unmounting) directly against a container element.

Both accept every field from the shared options reference (points, layout, path, wrap?, mobileBehavior?), plus the same caption/captionPosition fields described on the React page — this page covers what's specific to the vanilla binding.

renderTimelineSVG

A pure function — no DOM required, safe to call during SSR.

function renderTimelineSVG<T>(
  options: TrailvineOptions<T> & { caption?: string; captionPosition?: 'top' | 'bottom' }
): string;

mobileBehavior: 'list' is fully supported here — both markups (SVG and the plain list) are emitted together, toggled by a scoped @media query, no JavaScript required.

mobileBehavior: 'vertical' is not fully supported by this function alone — a static string can't react to the viewport resizing after the fact. Use mountTimeline instead if you need that behavior.

mountTimeline

Mounts a rendered timeline into a real container element, with lifecycle management.

function mountTimeline<T>(
  container: HTMLElement,
  options: TrailvineOptions<T> & { caption?: string; captionPosition?: 'top' | 'bottom' }
): MountResult;

interface MountResult {
  unmount: () => void;
}

This is the only place mobileBehavior: 'vertical' genuinely works — it uses matchMedia (with a legacy Safari fallback) to re-run layout at a different orientation when the viewport crosses the breakpoint, which requires real, live browser access that a pure string function doesn't have.

Cleanup

Calling the returned unmount() clears the container and removes only that instance's listener — mounting two timelines and unmounting one has no effect on the other, each call creates its own closure-scoped listener.

const { unmount } = mountTimeline(document.getElementById('timeline'), options);

// later, e.g. in a framework's cleanup hook:
unmount();

Multi-instance safety

Every call gets its own scoped class/id prefix via a simple incrementing counter (not crypto.randomUUID() — this keeps the package dependency-free and works identically during SSR and in the browser).

Next steps