Custom Node Templates

By default ApexTree renders each node's name inside a simple card. Supply a nodeTemplate function to render arbitrary HTML inside every node instead.

nodeTemplate basics

nodeTemplate receives the node's resolved content (the value at contentKey) and returns an HTML string. The HTML is rendered inside an SVG <foreignObject>.

const tree = new ApexTree(document.getElementById('chart'), {
  nodeWidth: 160,
  nodeHeight: 60,
  nodeTemplate: (content) => `
    <div style="display:flex;align-items:center;justify-content:center;
                height:100%;font-weight:600;">
      ${content}
    </div>
  `,
})
tree.render(data)

The content argument

content is whatever contentKey points at on the node. With the default contentKey: 'name', it is the node's name string. Point contentKey at a data object to pass a richer payload:

const data = {
  id: 'ceo',
  name: 'Alice',
  data: { name: 'Alice Johnson', title: 'CEO', avatar: '/alice.jpg' },
  children: [],
}

const tree = new ApexTree(el, {
  contentKey: 'data',   // content is now the data object
  nodeWidth: 220,
  nodeHeight: 84,
  nodeTemplate: (content) => `
    <div style="display:flex;align-items:center;gap:12px;padding:0 12px;height:100%;">
      <img src="${content.avatar}" style="width:40px;height:40px;border-radius:50%;" />
      <div style="display:flex;flex-direction:column;">
        <span style="font-weight:600;">${content.name}</span>
        <span style="font-size:12px;color:#64748B;">${content.title}</span>
      </div>
    </div>
  `,
})
tree.render(data)

The context argument

The second argument carries layout-level settings so templates can adapt without reading globals:

FieldTypeDescription
directionTreeDirectionThe tree's growth direction
cardImagePosition'left' | 'top'Where the built-in card places the avatar
expandedbooleanWhether this node's card is expanded in place
lod'full' | 'compact' | 'dot'Level-of-detail tier at the current zoom

expanded is always false unless the card has been expanded, and lod is always 'full' unless semanticZoom.enabled is set, so a template that branches on either works whether or not the feature is on. The context argument is typed as optional, so read it defensively (context?.lod ?? 'full') in TypeScript.

nodeTemplate: (content, context) => {
  const stack = context.cardImagePosition === 'top' ? 'column' : 'row'
  return `
    <div style="display:flex;flex-direction:${stack};align-items:center;
                gap:8px;height:100%;justify-content:center;">
      <img src="${content.avatar}" style="width:36px;height:36px;border-radius:50%;" />
      <span style="font-weight:600;">${content.name}</span>
    </div>
  `
}

Decorating the wrapper instead

Sometimes you do not want to replace a node's content, only tag it. nodeWrapper stamps classes and attributes onto each node's wrapper <g> while the nodeTemplate keeps rendering the card:

const tree = new ApexTree(el, {
  nodeWrapper: ({ id, depth, collapsed, hasChildren }) => ({
    className: collapsed ? 'is-collapsed' : '',
    attributes: { 'data-depth': depth, 'data-menu-target': id },
  }),
})

The context carries id, name, data, depth, collapsed, expanded and hasChildren. Return { className, attributes }, or nothing to leave the wrapper untouched.

Attributes ApexTree owns for identity and layout (data-self, data-parent, data-x, data-y, data-sig, data-depth, class, transform) are ignored rather than overwritten, so a hook can never break the reconciler. Use className for classes rather than an attributes.class.

A per-node hook set through NestedNode.options.nodeWrapper wins over the global one.

Because stamping happens on the SVG group outside the foreignObject, it is free of the caveats below. This is the right place to hang a context menu, a drag handle, per-node styling hooks, or a framework boundary; attach your own delegated listeners on the container:

document.getElementById('chart').addEventListener('contextmenu', (e) => {
  const target = e.target.closest('[data-menu-target]')
  if (target) openMenu(target.dataset.menuTarget, e)
})

Safari foreignObject caveats

Because templates render inside a <foreignObject> under a scaled viewBox, certain CSS properties trigger a Safari paint bug where content collapses to the SVG origin. Avoid these on template elements:

  • position (relative / absolute / fixed / sticky)
  • opacity < 1
  • transform
  • filter
  • z-index
  • will-change
  • mix-blend-mode
  • isolation: isolate

Use flex/grid layout with DOM order and color-based dimming instead. ApexTree logs a one-time console.warn if a template trips this.

// Avoid:  <div style="position:relative;opacity:0.8;transform:scale(1.1)">
// Prefer: <div style="display:flex;color:#94A3B8">

Built-in org card (OrgNodeData)

Before writing a custom template, consider the built-in card. Point contentKey at an OrgNodeData object and ApexTree renders a professional card (avatar, name, title, subtitle, status chip, accent stripe) with no template code. See Data Format.

cardImagePosition controls where the built-in card places the avatar:

const tree = new ApexTree(el, {
  contentKey: 'data',
  cardImagePosition: 'top',   // avatar above the text ('left' by default)
  nodeWidth: 200,
  nodeHeight: 120,
})

Per-node templates via node options

A custom nodeTemplate set globally applies to every node. To vary a single node, override font, node, and tooltip options through that node's options field (see Data Format). For entirely different markup per node, branch inside the global template on the content.

React example

import { ApexTreeChart } from 'react-apextree'

const options = {
  contentKey: 'data',
  nodeWidth: 220,
  nodeHeight: 84,
  nodeTemplate: (content: any) => `
    <div style="display:flex;align-items:center;gap:12px;padding:0 12px;height:100%">
      <img src="${content.avatar}" style="width:40px;height:40px;border-radius:50%" />
      <div style="display:flex;flex-direction:column">
        <span style="font-weight:600">${content.name}</span>
        <span style="font-size:12px;color:#64748B">${content.title}</span>
      </div>
    </div>
  `,
}

export default function TemplatedTree() {
  return <ApexTreeChart data={data} options={options} />
}