Usage guidesElementsNodes

Custom node decorations

Measure and draw content while configuring placement, stacking, sorting, overflow, and collisions

Use builtin-badge for existing counts, icons, and text badges. Register DecorationRenderer for custom content; the engine owns positioning, stacking, and overflow.

Measure, draw, and register

measure returns full finite positive bounds. This example converts a fixed CSS font using DPR and caches calculations in layout.data. draw is centered on the decoration and explicitly sets fonts, colors, and alignment.

Loading example…
A has five items, initially two plus +3. Hover decorations to expand; leave to collapse. B is a single-item comparison
const pill: DecorationRenderer = {
  measure({ ctx, data, dpr, labelScale = dpr }) {
    const text = String(data ?? '');
    ctx.font = 12 * labelScale + 'px sans-serif';
    return { width: ctx.measureText(text).width + 12 * labelScale, height: 22 * labelScale, data: { text, unit: labelScale } };
  },
  draw({ ctx, layout }) {
    const value = layout.data as { text: string; unit: number } | undefined;
    if (!value) return;
    ctx.fillStyle = '#e6f4ff';
    ctx.fillRect(-layout.width / 2, -layout.height / 2, layout.width, layout.height);
    ctx.font = 12 * value.unit + 'px sans-serif';
    ctx.fillStyle = '#0958d9';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText(value.text, 0, 0);
  },
};
register('decoration', 'docs-pill', pill);

A has five items, B only Ready. A initially shows two items and +3. Hover its decoration region to expand all; leave to collapse. Expand full code for items and placement.

Position and stack

  1. Match renderer to its registration. Use data for one item and items for a stack.
  2. placement chooses the side; attachment chooses boundary or outside positioning.
  3. layout: 'stack' enables vertical items; sort, maxVisible, and overflow control visibility. maxVisible includes the overflow item: 3 shows two items plus +3 here.
  4. overflowData goes to the same renderer, which must also support that input.

Do not reimplement node positioning, sorting, or overflow inside the renderer. See decoration configuration for offsets, anchors, ratios, and defaults.

Collisions and updates

The example disables decoration collision hiding for comparison. In real data, choose by density; hideOnCollision: false can overlap and does not reserve decoration collision-grid space. minZoom is independent of label policy.

Update callback-driven items through public data methods, not shared renderer state. Input node dimensions are already in screen units. This example deliberately uses fixed CSS text and converts by dpr only. See DecorationRenderer for units.

Common questions

Why is there no fallback text?

Unknown renderers, invalid measurements, and exceptions skip decoration content rather than using Label fallback. Check diagnostics, minZoom, hidden states, and collisions.

Why did measurement not lead to drawing?

Culling, collision, or overflow selection can skip draw. Keep measurement pure rather than treating invocation counts as business state.

Can I use the built-in badge instead?

Yes; see node badges. Reuse existing visuals before implementing a renderer.

Related runnable examples