# Customize nodes

Render content keyed by `nodeId` while Grafloria keeps the node's identity, selection, ports, links, routing, and serialized data. Use this when a rectangle is not enough—for example, to show a service name, owner, and status inside a node.

## The model

Put the node's stable key in `id`, the renderer key in `type`, and application data in `data`. A [`NodeSpec`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-nodespec) holds that data and geometry; an [`EdgeSpec`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-edgespec) holds a connection. Keep the node's geometry in `position` and `size`; keep connections in `edges`. The custom content is the inside of the box. The diagram still owns hit-testing, dragging, ports, selection, and link geometry.

The component receives the node's `data` and its live selection state. In React, describe those values with [`NodeProps`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-react); in plain JavaScript, use the HTML-layer representation supplied by the node spec. A node with `type: 'card'` is rendered by the `card` entry in the framework's node registry or slot.

## Plain JavaScript

Use [`render`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-element-core) with a data-backed HTML description. See the [custom-nodes demo](https://grafloria.com/demos/nodes/custom-nodes.html) for the running result. `metadata.html` is sanitized and placed inside the node's transformed HTML layer, so moving the node moves its content with it. The `id` and `data` remain part of the ordinary node document.

```js
import { render } from '@grafloria/element';

const host = document.querySelector('#app');
if (!(host instanceof HTMLElement)) {
  throw new Error('The #app element is required');
}
host.style.height = '400px';
const nodeId = 'deploy';

const instance = render({
  nodes: [{
    id: nodeId,
    position: { x: 120, y: 100 },
    size: { width: 260, height: 140 },
    data: { nodeId, title: 'Deploy #4213', status: 'staging' },
    metadata: {
      html: {
        content: {
          tag: 'div',
          children: [
            { tag: 'strong', text: nodeId + ': Deploy #4213' },
            { tag: 'p', text: 'building → 72%' },
            { tag: 'span', className: 'badge', text: 'staging' },
          ],
        },
      },
    },
  }],
  edges: [],
}, host);

instance.fitView();
```

The mounted diagram shows a card containing “Deploy #4213”, “building → 72%”, and a “staging” badge. The card remains a diagram node, rather than a floating element, when the user pans or drags it. Give `#app` a real height; a host without height has no canvas area to paint.

## Framework bindings

The following examples use the same two nodes and one link. Each custom node is keyed by `type: 'card'`, while the application values are keyed in `data.nodeId`; the body can choose its content from that key. In the framework bindings, [`GrafloriaFlow`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-react) mounts the diagram. The container has a height so the mounted diagram is visible.

:::code-group
```tsx title="React"
import { useEffect } from 'react';
import { GrafloriaFlow, type NodeProps } from '@grafloria/react';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

type CardData = { nodeId: string; title: string; owner: string; status: string };

const nodes: NodeSpec[] = [
  { id: 'build', type: 'card', custom: true, position: { x: 80, y: 90 }, size: { width: 230, height: 110 }, data: { nodeId: 'build', title: 'Build', owner: 'CI', status: 'passing' } },
  { id: 'deploy', type: 'card', custom: true, position: { x: 430, y: 90 }, size: { width: 230, height: 110 }, data: { nodeId: 'deploy', title: 'Deploy', owner: 'CD', status: 'ready' } },
];
const edges: EdgeSpec[] = [{ id: 'build-deploy', source: 'build', target: 'deploy' }];

function Card({ data, selected }: NodeProps<CardData>) {
  return <div style={{ height: '100%', boxSizing: 'border-box', padding: 14, border: '2px solid #94A5F0', borderRadius: 12, background: selected ? '#eef2ff' : '#fff' }}>
    <strong>{data.title}</strong><div>{data.nodeId}</div><div>owner: {data.owner}</div><span>{data.status}</span>
  </div>;
}

export default function App() {
  useEffect(() => undefined, []);
  return <div style={{ height: '400px' }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} nodeTypes={{ card: Card }} /></div>;
}
```
```vue title="Vue"
<script setup lang="ts">
import { GrafloriaFlow } from '@grafloria/vue';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'build', type: 'card', position: { x: 80, y: 90 }, size: { width: 230, height: 110 }, data: { nodeId: 'build', title: 'Build', owner: 'CI', status: 'passing' } },
  { id: 'deploy', type: 'card', position: { x: 430, y: 90 }, size: { width: 230, height: 110 }, data: { nodeId: 'deploy', title: 'Deploy', owner: 'CD', status: 'ready' } },
];
const edges: EdgeSpec[] = [{ id: 'build-deploy', source: 'build', target: 'deploy' }];
</script>

<template>
  <div style="height:400px"><GrafloriaFlow :default-nodes="nodes" :default-edges="edges">
    <template #node-card="{ data }"><div style="height:100%; box-sizing:border-box; padding:14px; border:2px solid #94A5F0; border-radius:12px; background:#fff">
      <strong>{{ data.title }}</strong><div>{{ data.nodeId }}</div><div>owner: {{ data.owner }}</div><span>{{ data.status }}</span>
    </div></template>
  </GrafloriaFlow></div>
</template>
```
```tsx title="Qwik"
import { component$ } from '@builder.io/qwik';
import { GrafloriaFlow, type EdgeSpec, type NodeProps, type NodeSpec } from '@grafloria/qwik';

type CardData = { nodeId: string; title: string; owner: string; status: string };

const Card = component$(({ data, selected }: NodeProps<CardData>) => (
  <div style={{ height: '100%', boxSizing: 'border-box', padding: '14px', border: '2px solid #94A5F0', borderRadius: '12px', background: selected ? '#eef2ff' : '#fff' }}>
    <strong>{data.title}</strong><div>{data.nodeId}</div><div>owner: {data.owner}</div><span>{data.status}</span>
  </div>
));

const nodes: NodeSpec[] = [
  { id: 'build', type: 'card', custom: true, position: { x: 80, y: 90 }, size: { width: 230, height: 110 }, data: { nodeId: 'build', title: 'Build', owner: 'CI', status: 'passing' } },
  { id: 'deploy', type: 'card', custom: true, position: { x: 430, y: 90 }, size: { width: 230, height: 110 }, data: { nodeId: 'deploy', title: 'Deploy', owner: 'CD', status: 'ready' } },
];
const edges: EdgeSpec[] = [{ id: 'build-deploy', source: 'build', target: 'deploy' }];

export default component$(() => (
  <div style={{ height: '400px' }}>
    <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} nodeTypes={{ card: Card }} />
  </div>
));
```
```ts title="Angular — custom-nodes.component.ts"
import { Component } from '@angular/core';
import { DiagramCanvasComponent, GrafloriaNodeDefDirective } from '@grafloria/angular';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent, GrafloriaNodeDefDirective],
  template: `
    <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges" style="display:block; height:400px">
      <ng-template grafloriaNode="card" let-data="data">
        <div style="height:100%; box-sizing:border-box; padding:14px; border:2px solid #94A5F0; border-radius:12px; background:#fff">
          <strong>{{ data['title'] }}</strong><div>{{ data['nodeId'] }}</div><div>owner: {{ data['owner'] }}</div><span>{{ data['status'] }}</span>
        </div>
      </ng-template>
    </grafloria-diagram-canvas>
  `,
})
export class CustomNodesComponent {
  nodes: NodeSpec[] = [
  { id: 'build', type: 'card', position: { x: 80, y: 90 }, size: { width: 230, height: 110 }, data: { nodeId: 'build', title: 'Build', owner: 'CI', status: 'passing' } },
  { id: 'deploy', type: 'card', position: { x: 430, y: 90 }, size: { width: 230, height: 110 }, data: { nodeId: 'deploy', title: 'Deploy', owner: 'CD', status: 'ready' } },
  ];
  edges: EdgeSpec[] = [{ id: 'build-deploy', source: 'build', target: 'deploy' }];
}
```
:::

React requires `custom: true` on the spec when the node is rendered by `nodeTypes`. Vue slots and Angular templates opt into the HTML layer through their bindings. In every case, the custom body remains inside the node host, so the link stays attached when the node moves and the node remains selectable and connectable.

## Updating and persisting the content

Keep the node key stable when application data changes. For a live model obtained from the instance, update through the model's tracked setters and then call [`DiagramInstance`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-diagraminstance)`renderNow()` when you need the repaint immediately; do not replace the host's geometry styles. In plain JavaScript, custom renderer content mounts once, so update DOM that your renderer owns or use a framework binding that re-renders from its data.

The node's `data`, `id`, geometry, and `type` are part of the document. Keep those values in your `NodeSpec` and edges in [`EdgeSpec`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-edgespec); the custom component is presentation, not a second model. This lets the same diagram remain routable and round-trip through serialization.

## Pitfalls

- A plain JavaScript or React custom node without `custom: true` renders as a normal rectangle; the renderer or component is not consulted. Vue slots and Angular templates opt in automatically.
- Register or provide the node type before mounting. An unknown custom type leaves an empty host rather than creating a built-in body.
- Put geometry in `position: { x, y }` and `size: { width, height }`; top-level `x` and `y` are not node-spec fields.
- Interpolate user-supplied values as text. Do not place them in `innerHTML`.
- A custom node does not replace ports. Use ordinary edges and declared ports when connection direction or routing matters.

## See also

- [How Grafloria works](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/how-grafloria-works)
- [Edit Mermaid diagrams](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/edit-mermaid-diagrams)
- [The `DiagramInstance`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-diagraminstance)
