# Create custom nodes

Use a custom node when a node needs content beyond Grafloria's built-in label and shape. Declare nodes as [`NodeSpec`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-nodespec#nodespec) values and connect them with [`EdgeSpec`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-edgespec#edgespec) edges. The framework bindings put your own component or template inside the node; the diagram still owns its geometry, hit-testing, selection and connections. The plain JavaScript example uses a structured HTML body inside an SVG `foreignObject`.

## 1. Render content inside a node

Give each node a `type`, a real `size`, and data for its content. Then connect node IDs with edges. In React, register a component for the type and set `custom: true`; Vue and Angular use the matching type declaration to opt into framework templates. Plain JavaScript and Qwik can use the structured `metadata.html` body shown below.

The sample renders two connected service cards. Each framework's node body stays inside its box as the diagram moves and zooms; the link remains attached to the node geometry. The HTML-node demo shows a card with a heading, progress line and badge inside its node.

The examples use these node fields:

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `type` | `string` | `'rect'` | Selects the custom-node type rendered by a matching component, slot or template. |
| `size` | `{ width: number; height: number }` | Not specified | Sets the node box that contains the custom content. |
| `data` | `Record<string, any>` | Not specified | Carries the node payload to custom components and templates. |
| `custom` | `boolean` | Not specified | On React nodes, `true` routes the node through the HTML layer. Vue and Angular also opt in from a matching declared type. |

### Framework examples

Use [`render`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-element-core#render) in the browser to mount a data spec. For structured HTML, put a sanitized content tree in `metadata.html.content`; the body renders inside the node's `foreignObject`, so it moves with the node group. The code creates a host with a real height.

Angular's [`DiagramCanvasComponent`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-angular-components-diagramcanvascomponent#diagramcanvascomponent) and exact `grafloriaNode` template, Qwik's [`GrafloriaFlow`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-qwik#grafloriaflow), React's [`GrafloriaFlow`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-react#grafloriaflow), and Vue's [`GrafloriaFlow`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-vue#grafloriaflow) and named slot are the framework front doors. Angular and Vue opt matching template types in automatically; React uses both a `nodeTypes` entry and `custom: true`. In Qwik, mount the flow with Qwik's renderer and pass nodes with structured HTML metadata.

:::code-group
```js title="JavaScript"
import { render } from '@grafloria/element';

const nodes = [
  {
    id: 'api',
    position: { x: 80, y: 100 },
    size: { width: 210, height: 100 },
    metadata: {
      html: {
        content: {
          tag: 'div',
          className: 'service-card',
          children: [
            { tag: 'strong', text: 'API gateway' },
            { tag: 'span', className: 'status', text: 'Healthy' },
          ],
        },
      },
    },
  },
  {
    id: 'worker',
    position: { x: 400, y: 100 },
    size: { width: 210, height: 100 },
    metadata: {
      html: {
        content: {
          tag: 'div',
          className: 'service-card',
          children: [
            { tag: 'strong', text: 'Order worker' },
            { tag: 'span', className: 'status', text: 'Running' },
          ],
        },
      },
    },
  },
];

const edges = [{ id: 'api-worker', source: 'api', target: 'worker' }];
const target = document.createElement('div');
target.style.height = '420px';
target.style.width = '100%';
target.id = 'diagram';
document.body.append(target);

const style = document.createElement('style');
style.textContent = '#diagram .service-card { font: 14px/1.4 system-ui, sans-serif; padding: 12px; } #diagram .service-card strong { display: block; } #diagram .service-card .status { color: #059669; font-size: 12px; }';
document.head.append(style);

const instance = render({ nodes, edges }, target);
instance.fitView();
```
```ts title="Angular"
import { Component } from '@angular/core';
import { DiagramCanvasComponent, GrafloriaNodeDefDirective } from '@grafloria/angular';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

@Component({
  selector: 'app-board',
  standalone: true,
  imports: [DiagramCanvasComponent, GrafloriaNodeDefDirective],
  template: `
    <grafloria-diagram-canvas [nodes]="nodes" [edges]="edges"
      style="display:block; height:420px">
      <ng-template grafloriaNode="service" let-data="data">
        <div class="service-card">
          <strong>{{ data['name'] }}</strong>
          <span>{{ data['status'] }}</span>
        </div>
      </ng-template>
    </grafloria-diagram-canvas>
  `,
  styles: [`.service-card { height: 100%; box-sizing: border-box; padding: 12px;
    border: 1px solid #94a5f0; border-radius: 10px; background: white; }
    .service-card strong, .service-card span { display: block; }
    .service-card span { color: #059669; font-size: 12px; }`],
})
export class BoardComponent {
  nodes: NodeSpec[] = [
    { id: 'api', type: 'service', position: { x: 80, y: 100 }, size: { width: 210, height: 100 }, data: { name: 'API gateway', status: 'Healthy' } },
    { id: 'worker', type: 'service', position: { x: 400, y: 100 }, size: { width: 210, height: 100 }, data: { name: 'Order worker', status: 'Running' } },
  ];
  edges: EdgeSpec[] = [{ id: 'api-worker', source: 'api', target: 'worker' }];
}
```
```tsx title="Qwik"
import { render as renderQwik } from '@builder.io/qwik';
import { GrafloriaFlow, type EdgeSpec, type NodeSpec } from '@grafloria/qwik';

const nodes: NodeSpec[] = [
  {
    id: 'api',
    position: { x: 80, y: 100 },
    size: { width: 210, height: 100 },
    metadata: {
      html: {
        content: {
          tag: 'div',
          children: [
            { tag: 'strong', text: 'API gateway' },
            { tag: 'span', text: 'Healthy' },
          ],
        },
      },
    },
  },
  {
    id: 'worker',
    position: { x: 400, y: 100 },
    size: { width: 210, height: 100 },
    metadata: {
      html: {
        content: {
          tag: 'div',
          children: [
            { tag: 'strong', text: 'Order worker' },
            { tag: 'span', text: 'Running' },
          ],
        },
      },
    },
  },
];
const edges: EdgeSpec[] = [{ id: 'api-worker', source: 'api', target: 'worker' }];

const target = typeof document === 'undefined' ? null : document.createElement('div');
if (target) {
  target.style.height = '420px';
  document.body.append(target);
  void renderQwik(target, <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} fitView />);
}
```
```tsx title="React"
import { GrafloriaFlow } from '@grafloria/react';
import type { EdgeSpec, NodeProps, NodeSpec, NodeTypes } from '@grafloria/react';

function ServiceNode({ id, selected }: NodeProps<never>) {
  return (
    <div style={{
      boxSizing: 'border-box',
      width: '100%',
      height: '100%',
      padding: '12px',
      border: selected ? '2px solid #3b52d9' : '1px solid #94a5f0',
      borderRadius: 10,
      background: '#fff',
      font: '14px/1.4 system-ui, sans-serif',
    }}>
      <strong>{id}</strong>
      <div>Service node</div>
    </div>
  );
}

const nodeTypes: NodeTypes = { service: ServiceNode };
const nodes: NodeSpec[] = [
  { id: 'api', type: 'service', custom: true, position: { x: 80, y: 100 }, size: { width: 210, height: 100 } },
  { id: 'worker', type: 'service', custom: true, position: { x: 400, y: 100 }, size: { width: 210, height: 100 } },
];
const edges: EdgeSpec[] = [{ id: 'api-worker', source: 'api', target: 'worker' }];

export function Board() {
  return (
    <div style={{ height: '420px' }}>
      <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} nodeTypes={nodeTypes} fitView />
    </div>
  );
}
```
```vue title="Vue"
<script setup lang="ts">
import { GrafloriaFlow, type EdgeSpec, type NodeSpec } from '@grafloria/vue';

const nodes: NodeSpec[] = [
  { id: 'api', type: 'service', position: { x: 80, y: 100 }, size: { width: 210, height: 100 }, data: { name: 'API gateway', status: 'Healthy' } },
  { id: 'worker', type: 'service', position: { x: 400, y: 100 }, size: { width: 210, height: 100 }, data: { name: 'Order worker', status: 'Running' } },
];
const edges: EdgeSpec[] = [{ id: 'api-worker', source: 'api', target: 'worker' }];
</script>

<template>
  <div style="height: 420px">
    <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" fit-view>
      <template #node-service="{ data }">
        <div class="service-card">
          <strong>{{ data['name'] }}</strong>
          <span>{{ data['status'] }}</span>
        </div>
      </template>
    </GrafloriaFlow>
  </div>
</template>

<style>
.service-card { box-sizing: border-box; height: 100%; padding: 12px;
  border: 1px solid #94a5f0; border-radius: 10px; background: white; }
.service-card strong, .service-card span { display: block; }
.service-card span { color: #059669; font-size: 12px; }
</style>
```
:::

In plain JavaScript, `render()` returns the mounted instance and `fitView()` frames both cards and their connecting edge. Grafloria sanitizes the structured tree rather than inserting it as raw HTML.

Angular renders the template with the node's `data` payload inside the sized canvas. Read payload keys with index syntax because the template context uses `Record<string, unknown>`. The `GrafloriaNodeDefDirective` import is required for `grafloriaNode` to match.

Qwik's renderer mounts the flow in the browser when `document` exists. The flow shows the two cards from their structured HTML trees and connects them with the edge.

React renders a custom component for both nodes; each card shows its node ID and a selection-aware border, and each React spec sets `custom: true`.

Vue renders the named slot for each matching node and exposes its data in the slot scope. Both service cards fill their node boxes while the edge connects them.

## 2. Keep the content within the node box

Set `size` on each node; the diagram uses that geometry to size and position the custom-content host. Make the root element fill the box with `height: 100%` and `box-sizing: border-box`, so padding and borders fit within the node's hit area. Give the canvas parent a real height as well.

In React, a missing or mismatched `custom: true` flag or `nodeTypes` key means the custom component does not render. In Vue and Angular, match the type exactly to the named slot or template directive. For Angular, include `GrafloriaNodeDefDirective` in the component's `imports`; without it the `grafloriaNode` template is ignored.

The JavaScript `metadata.html` route is sanitized and structured. Supply element descriptions and text values rather than interpolating untrusted values into raw HTML.

## 3. See the nodes running

- [Live HTML-node demo](https://grafloria.com/demos/nodes/html-nodes.html) — structured HTML rendered inside a node.
- [Custom-node demo](https://grafloria.com/demos/nodes/custom-nodes.html) — built-in terminal, predefined-process and document shapes connected in one diagram.
- [Custom-node demo source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/nodes/custom-nodes.html)

If you need a different built-in silhouette or fill rather than custom content, use the per-node `shape` option; the custom-node demo shows terminal, predefined-process and document silhouettes. For node sizing and geometry, see [Resize and size nodes](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/resize-and-size-nodes).
