Expand and Collapse

Nodes with children can be collapsed to hide their subtree and expanded to reveal it. ApexTree provides a dedicated button, an optional click-to-toggle mode, count badges, batch verbs, and programmatic control.

Expand/collapse button

enableExpandCollapse (on by default) shows a +/- button on every node that has children:

const tree = new ApexTree(document.getElementById('chart'), {
  enableExpandCollapse: true,   // default
})
tree.render(data)

Style the button with:

OptionTypeDefaultDescription
expandCollapseButtonBGColorstring'#FFFFFF'Button background
expandCollapseButtonBorderColorstring'#E4E7EC'Button border
expandCollapseButtonIconColorstring'#475467'Color of the +/- glyph
expandCollapseButtonSizenumber15Button diameter in pixels
expandCollapseButtonHaloColorstring''Opaque ring drawn outside the button

expandCollapseButtonSize is purely visual. The button carries an invisible hit area of max(size + 4, 24), so shrinking it never drops the tap target below the WCAG 2.2 SC 2.5.8 minimum of 24px. The chrome grows 15% on hover about its own centre while the hit area stays put, so the target never moves out from under the pointer.

Set expandCollapseButtonIconColor whenever you change expandCollapseButtonBGColor, or the glyph can end up invisible against the button fill.

The button straddles the node's edge, and its border shares a default grey with borderColor, so on a busy canvas it can dissolve into the node border and the incoming edge. expandCollapseButtonHaloColor draws an opaque ring just outside it to punch a clean hole through both. It is opt-in because the library cannot know what is painted behind the nodes:

const tree = new ApexTree(el, {
  expandCollapseButtonHaloColor: '#F9FAFB',   // your page/container background
})

Click-to-toggle

expandCollapseOnNodeClick makes the entire node body toggle its expansion, in addition to the dedicated button. The cursor becomes a pointer on toggleable nodes:

const tree = new ApexTree(el, {
  expandCollapseOnNodeClick: true,
})
tree.render(data)

The toggle fires before onNodeClick, so your click callback sees the post-toggle state.

Collapse-count badge

When a node is collapsed, its hidden-descendant count renders inside the expand/collapse button, which widens into a pill:

OptionTypeDefaultDescription
collapseBadgeEnabledbooleantrueShow the badge on collapsed nodes
collapseBadgeThresholdnumber1Minimum hidden children before the badge appears
collapseBadgeBGColorstring'#5C6BC0'Badge background
collapseBadgeFontColorstring'#FFFFFF'Badge text color
collapseBadgeFontSizestring'12px'Badge font size
const tree = new ApexTree(el, {
  collapseBadgeEnabled: true,
  collapseBadgeThreshold: 3,   // only show the badge when 3+ children are hidden
  collapseBadgeBGColor: '#F59E0B',
})

collapseBadgeFontSize is capped at 70% of expandCollapseButtonSize so a themed value cannot overflow the pill. The --apex-tree-badge-* CSS variables style the same pill.

Always-on count badges

The collapse badge only appears on a collapsed node. countBadgeEnabled shows a persistent badge on every node instead, independent of collapse state:

OptionTypeDefaultDescription
countBadgeEnabledbooleanfalseShow a count badge on every node
countBadgeSource'descendants' | 'children' | 'data''descendants'What the number represents
countBadgeDataKeystring'count'Field read when countBadgeSource is 'data'
countBadgeThresholdnumber1Minimum count before the badge appears
countBadgeBGColorstring'#EEF2FF'Badge background
countBadgeFontColorstring'#3730A3'Badge text color
countBadgeFontSizestring'12px'Badge font size
const tree = new ApexTree(el, {
  countBadgeEnabled: true,
  countBadgeSource: 'children',   // direct reports, not the whole subtree
})

With countBadgeSource: 'data', the value comes from each node's own data at countBadgeDataKey and is coerced with Number(...). Non-numeric or missing values render no badge, which lets you show a domain metric such as open headcount or ticket count:

const tree = new ApexTree(el, {
  countBadgeEnabled: true,
  countBadgeSource: 'data',
  countBadgeDataKey: 'openRoles',
})

Programmatic expand/collapse

Call expand(nodeId) and collapse(nodeId) on the graph returned by render():

const graph = tree.render(data)

graph.collapse('vp2')    // hide vp2's subtree
graph.expand('vp2')      // reveal it again

Use getNodeMap() to traverse and act on specific nodes.

Batch verbs

Rather than looping over ids, use the batch verbs. Each runs as a single reflow, so the whole set springs together in one wave instead of one render per node:

MethodDescription
expandAll()Expand every node. No-op if nothing is collapsed
collapseAll()Collapse every node, leaving only the root visible
expandToDepth(depth)Show the tree down to depth (root = 0)
expandSubtree(nodeId)Expand a node and every one of its descendants
collapseSubtree(nodeId)Collapse a node and every one of its descendants
const graph = tree.render(data)

document.getElementById('expand-all').onclick   = () => graph.expandAll()
document.getElementById('collapse-all').onclick = () => graph.collapseAll()
document.getElementById('two-levels').onclick   = () => graph.expandToDepth(2)

collapseAll() leaves each level's own collapsed state intact, so a later expand reveals one level at a time. collapseSubtree(id) behaves the same way for one branch.

The batch verbs walk the tree as it currently exists, so they do not trigger fetches for nodes behind an unloaded hasChildren: true. See Lazy Children.

Auto-zoom on toggle

enableExpandCollapseZoom (on by default) re-fits the viewBox to the visible nodes after each expand/collapse, springing to the new framing. Set it to false to keep the viewport fixed:

const tree = new ApexTree(el, {
  enableExpandCollapseZoom: false,   // don't auto-fit after toggling
})

maxZoomNodeSpan (default 8) caps how far that fit can zoom in, so collapsing down to a couple of nodes does not balloon them to fill the canvas. Set it to 0 for a tight fit. See Zoom, Pan and Export.

Grouping leaf nodes

groupLeafNodes stacks leaf nodes vertically instead of spreading them horizontally — useful for wide trees with many terminal nodes:

const tree = new ApexTree(el, {
  groupLeafNodes: true,
  groupLeafNodesSpacing: 12,   // gap between stacked leaves (default: 10)
})
tree.render(data)

Grouped leaves are connected with an orthogonal side-bracket connector regardless of the global edgeStyle.

Animation

Expand and collapse are spring-driven when enableAnimation is true (the default). Entering nodes grow out of their parent, exiting nodes retract into it, edges are derived from their endpoints every frame, and the camera glides. A toggle that arrives while another is still in flight redirects the motion rather than restarting it.

Set enableAnimation: false for instant toggles. See Motion and Animation for motion.spring, the wave stagger, and reduced-motion handling.

Complete example

const tree = new ApexTree(document.getElementById('chart'), {
  enableExpandCollapse: true,
  expandCollapseOnNodeClick: true,
  collapseBadgeEnabled: true,
  collapseBadgeThreshold: 1,
  enableExpandCollapseZoom: true,
  enableAnimation: true,
})

const graph = tree.render(data)

document.getElementById('collapse-all').onclick = () => graph.collapseAll()
document.getElementById('expand-all').onclick   = () => graph.expandAll()

React example

import { useRef } from 'react'
import { ApexTreeChart } from 'react-apextree'
import type { ApexTreeRef } from 'react-apextree'

export default function CollapsibleTree() {
  const ref = useRef<ApexTreeRef>(null)

  return (
    <div>
      <button onClick={() => ref.current?.collapse('vp2')}>Collapse vp2</button>
      <button onClick={() => ref.current?.expand('vp2')}>Expand vp2</button>
      <ApexTreeChart
        ref={ref}
        data={data}
        options={{ expandCollapseOnNodeClick: true, collapseBadgeThreshold: 2 }}
      />
    </div>
  )
}