Motion and Animation

Collapse, expand, layout changes and data updates are driven by springs rather than tweened keyframes. A single animation ticker advances persistent per-node springs, and the render path reconciles the existing DOM instead of rebuilding it. Nodes travel to their new positions, connectors bend continuously with them, and a gesture that arrives mid-flight redirects instead of snapping.

What animates

Five things are spring-driven:

LayerBehavior
Node positionEvery node keeps its own spring and travels to its new place
Enter and exitEntering nodes grow out of their parent; exiting nodes retract into it
Edge geometryEach edge is derived from its two endpoint springs every frame
CameraThe viewBox is four springs, so fit, focus and zoom all glide
StaggerThe reflow radiates outward from the toggled node

Because an edge is recomputed from its endpoints rather than animated on its own timeline, it has no length while both ends are still stacked together, and grows only as they separate. Lines never arrive before the nodes they connect.

Tuning the spring

const tree = new ApexTree(document.getElementById('chart'), {
  direction: 'top',
  motion: { spring: 'crisp', stagger: 'wave' },
})
const graph = tree.render(data)

motion.spring picks the integrator's stiffness and damping:

ValueFeel
'crisp' (default)Settles quickly with no visible overshoot
'gentle'Softer and slower, which suits large reflows
'snappy'Faster and tighter

motion.stagger controls how a reflow propagates:

ValueBehavior
'wave' (default)Nodes are delayed by their graph distance from the pivot, so a subtree unfolds outward as a wave
'none'Every node moves together

Springs preserve velocity across a retarget, so toggling a second node while the first is still moving redirects the motion smoothly rather than restarting it.

First render

A first render seeds every node on the root and springs them outward, staggered by depth, through the same reveal an expand uses. This is a change from earlier versions, which placed every node at its final position and faded it in.

One consequence matters if you read layout from the DOM: data-x / data-y and a foreignObject's x / y return the seed position immediately after render(), not the settled layout. Either wait for the springs to settle, or disable animation for that instance:

// positions are final synchronously
new ApexTree(el, { enableAnimation: false }).render(data)

Grouped leaves

Trees using groupLeafNodes: true run through the same engine with no full-rebuild fallback. The side-bracket connector is a multi-point edge recomputed from the live parent and leaf springs each frame, so it morphs with the stack rather than jumping to a new shape.

Batch operations

expandAll(), collapseAll(), expandToDepth(n), expandSubtree(id) and collapseSubtree(id) each reflow once, so the whole set springs together in a single wave instead of one render per node. See Expand and Collapse.

Reduced motion

The tree honours a reduced-motion preference. Add the apextree-reduced-motion class to the container, or wire it to matchMedia, and animations are skipped with elements shown at their final state:

const el = document.getElementById('chart')
const mq = window.matchMedia('(prefers-reduced-motion: reduce)')

const sync = () => el.classList.toggle('apextree-reduced-motion', mq.matches)
sync()
mq.addEventListener('change', sync)

The marching dash used by the active path respects this too: the path still highlights, it just does not animate.

Turning motion off

enableAnimation: false renders and reflows with no motion at all, landing directly at the final state:

const tree = new ApexTree(el, { enableAnimation: false })

This is the right setting for tests, for server-rendered snapshots, and anywhere you measure geometry from the DOM straight after a render.

Camera fit

Expand and collapse re-fit the camera when enableExpandCollapseZoom is on. maxZoomNodeSpan (default 8) caps how far that fit can zoom in, so a collapsed view of two remaining nodes does not balloon to fill the canvas. Set it to 0 for a tight fit, or enableExpandCollapseZoom: false to keep the viewBox fixed. See Zoom, Pan and Export.