# Qwik: custom content and kits

Keep custom nodes and widgets self-contained, pass their content through typed data props, and build function-bearing kits in the browser with `spec$`. Use this pattern when a node needs application-specific HTML or a kit needs to survive server rendering and resumption.

Your custom component paints the inside of an engine-positioned box; the engine and renderer still own dragging, selection and connections. Start in a Qwik 1.x project using `@builder.io/qwik` ^1.5.0 and its optimizer. See the [Qwik quick start](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/qwik-quick-start) for setup.

```bash
npm install @grafloria/qwik @grafloria/engine @grafloria/renderer @grafloria/element @builder.io/qwik
```

## 1. Pass node content through typed props

Unlike React, registering a component in [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#grafloriaflow)'s [`NodeTypes`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#nodetypes) map automatically opts matching nodes into HTML rendering unless they specify `custom: false`; see [React: custom content](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/react-custom-content) for the shared type-to-component mapping.

The Qwik sample uses [`NodeProps`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#nodeprops) with a `ServiceData` payload to carry owner and status across the container boundary, alongside typed [`NodeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-nodespec#nodespec) and [`EdgeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-edgespec#edgespec) inputs; see [React: custom content](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/react-custom-content) for the shared props contract.

Custom nodes mount in separate Qwik containers. They cannot read contexts from the surrounding application, including router contexts. Pass the values they need through `node.data` rather than calling a parent-context hook inside the card. This is Qwik's container behavior, not a wrapper defect.

```tsx title="service-flow.tsx"
import { component$ } from '@builder.io/qwik';
import {
  GrafloriaFlow,
  type NodeProps,
  type NodeTypes,
  type NodeSpec,
  type EdgeSpec,
} from '@grafloria/qwik';

interface ServiceData {
  name: string;
  owner: string;
  status: 'healthy' | 'degraded';
}

const ServiceNode = component$((props: NodeProps<ServiceData>) => (
  <div style={{
    boxSizing: 'border-box', width: '100%', height: '100%',
    padding: '12px', borderRadius: '10px', background: '#ffffff',
    border: `2px solid ${props.data.status === 'healthy' ? '#059669' : '#d97706'}`,
    color: '#232a3d', font: '13px/1.4 system-ui, sans-serif',
  }}>
    <strong>{props.data.name}</strong>
    <div>{props.data.owner}</div>
    <div>{props.data.status}</div>
  </div>
));

const nodeTypes = { service: ServiceNode } satisfies NodeTypes;
const gateway: ServiceData = {
  name: 'api-gateway', owner: 'platform', status: 'healthy',
};
const orders: ServiceData = {
  name: 'orders-svc', owner: 'commerce', status: 'degraded',
};
const nodes: NodeSpec[] = [
  { id: 'gateway', type: 'service', position: { x: 60, y: 80 },
    size: { width: 190, height: 96 }, data: gateway },
  { id: 'orders', type: 'service', position: { x: 370, y: 80 },
    size: { width: 190, height: 96 }, data: orders },
];
const edges: EdgeSpec[] = [
  { id: 'gateway-orders', source: 'gateway', target: 'orders',
    sourceHandle: 'right', targetHandle: 'left' },
];

export default component$(() => (
  <GrafloriaFlow
    defaultNodes={nodes} defaultEdges={edges} nodeTypes={nodeTypes}
    fitView style={{ height: '400px' }}
  />
));
```

![Look at the green-bordered api-gateway and amber-bordered orders-svc cards: each displays its owner and status, and an arrow connects them.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/12ee0e9f14f826928827d0f7c387f677.png)

The canvas contains two connected service cards: a green-bordered healthy gateway and an amber-bordered degraded orders service. Each card fills the size declared on its node. The defaults seed an uncontrolled instance; this example does not mirror edits into application state.

## 2. Mix a custom widget with a shipped painter

[`GrafloriaDashboard`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#grafloriadashboard) uses the same container boundary. Map a widget's `kind` through [`WidgetTypes`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#widgettypes), and receive its full spec and data through [`WidgetProps`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#widgetprops). Feed it through `widget.data`; do not rely on surrounding application contexts.

Declare the board with [`DashboardWidgetSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-dashboard-kit-dashboardwidgetspec#dashboardwidgetspec). Only the `note` kind below has a custom component. The `kpi` kind uses the shipped painter, so you do not need to write a KPI renderer.

```tsx title="service-board.tsx"
import { component$ } from '@builder.io/qwik';
import {
  GrafloriaDashboard, type WidgetProps, type WidgetTypes,
} from '@grafloria/qwik';
import type { DashboardWidgetSpec } from '@grafloria/element';

interface NoteData {
  title: string;
  text: string;
}

const NoteWidget = component$((props: WidgetProps<NoteData>) => (
  <article style={{
    boxSizing: 'border-box', width: '100%', height: '100%',
    padding: '16px', background: '#fffbeb', color: '#78350f',
    font: '14px/1.5 system-ui, sans-serif',
  }}>
    <strong>{props.data.title}</strong>
    <p>{props.data.text}</p>
  </article>
));

const widgetTypes = { note: NoteWidget } satisfies WidgetTypes;
const note: NoteData = {
  title: 'Operations note', text: 'Check the orders service before the release.',
};
const widgets: DashboardWidgetSpec[] = [
  { id: 'services', kind: 'kpi', span: 3, rows: 1,
    data: { label: 'Healthy services', value: '2 / 3' } },
  { id: 'release-note', kind: 'note', span: 3, rows: 1, data: { ...note } },
];

export default component$(() => (
  <GrafloriaDashboard
    widgets={widgets} widgetTypes={widgetTypes}
    options={{ columns: 6, width: 800, height: 400, gap: 8 }}
    style={{ height: '400px' }}
  />
));
```

The board displays a Healthy services KPI beside an Operations note card. Qwik builds and mounts the dashboard kit in the browser; see [React: custom content](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/react-custom-content#respect-the-mount-once-board-props) for the shared mount-once input behavior. For runtime widget edits, see [Build a dashboard](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/build-a-dashboard).

## 3. Build a kit through `spec$` and remount for new data

Use [`GrafloriaDiagram`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#grafloriadiagram) for a kit rather than rebuilding its cards yourself. The shipped [`erDiagram`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-diagram-kit-functions#erdiagram) builder returns table-card specs and a `finalize` function. Its entities become HTML tables with typed columns and PK/FK badges; its relationships become orthogonal edges with crow's-foot cardinality.

Pass the builder through `spec$`, not `spec`. The QRL runs in the browser, so the function-bearing result never enters server-rendered resumable state. Plain-data specs and JSON/DSL strings can use `spec` instead. See [Documents and kits](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/documents-and-kits) for live-object serialization rules.

A `$` prop stays fixed for the life of its component. Capture a data snapshot and change the component's `key` when you want a new diagram. This example starts with Customer and Order tables; Add email rebuilds them with an extra Customer column. The `onReady$` callback receives the mounted [`DiagramInstance`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-diagraminstance#diagraminstance), and `fitView(40)` frames its content.

Type the schema data with [`ErEntitySpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-diagram-kit-interfaces#erentityspec) and [`ErRelationshipSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-diagram-kit-interfaces#errelationshipspec).

```tsx title="schema-diagram.tsx"
import { component$, useSignal } from '@builder.io/qwik';
import { GrafloriaDiagram, type DiagramInstance } from '@grafloria/qwik';
import { erDiagram, type ErEntitySpec, type ErRelationshipSpec } from '@grafloria/element';

export default component$(() => {
  const schemaVersion = useSignal(0);
  const version = schemaVersion.value;
  const entities: ErEntitySpec[] = [
    { id: 'CUSTOMER', name: 'Customer', position: { x: 60, y: 80 },
      columns: [
        { name: 'id', type: 'int', pk: true },
        { name: 'name', type: 'varchar' },
        ...(version > 0 ? [{ name: 'email', type: 'varchar' }] : []),
      ] },
    { id: 'ORDER', name: 'Order', position: { x: 420, y: 80 },
      columns: [
        { name: 'id', type: 'int', pk: true },
        { name: 'customer_id', type: 'int', fk: true },
      ] },
  ];
  const relationships: ErRelationshipSpec[] = [
    { from: 'CUSTOMER', to: 'ORDER', label: 'places' },
  ];

  return (
    <section>
      <button disabled={version > 0} onClick$={() => { schemaVersion.value += 1; }}>
        Add email
      </button>
      <GrafloriaDiagram
        key={version}
        spec$={() => erDiagram({ entities, relationships })}
        onReady$={(instance: DiagramInstance) => { instance.fitView(40); }}
        style={{ height: '400px' }}
      />
    </section>
  );
});
```

![Look at the CUSTOMER and ORDER tables, their PK/FK badges, and the places relationship with a crow's-foot at ORDER. The Add email button sits above the initial diagram.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/03558c35e914bad6652c2eaa91d1bbb4.png)

The initial diagram contains two tables joined by a relationship labelled places. Add email replaces the mounted diagram; it is a rebuild, not an undoable edit of the old instance. The wrapper disposes the old instance on unmount, so do not retain it for later calls.

For editing the existing document instead of replacing it, see [Edit database models](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/edit-database-models).

## Options that matter

[`RenderSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#renderspec) is the shared render-input type used by `spec` and `spec$`.

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `nodeTypes` | `NodeTypes` | No registrations | Maps node types to Qwik components and opts matching specs into custom rendering. |
| `NodeSpec.custom` | `boolean` | Inferred for registered types by the binding | An explicit value overrides the inferred custom flag. |
| `widgetTypes` | `WidgetTypes` | No registrations | Maps widget kinds to Qwik components; unmatched kinds use shipped painters. |
| `spec` | `RenderSpec` | Not set | Supplies plain data or text to the generic host. |
| `spec$` | `QRL<() => RenderSpec \| Promise<RenderSpec>>` | Not set | Builds the spec in the browser; takes precedence over `spec`. |

Give a canvas a resolved height as shown above; see [Theme a canvas](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/theme-a-canvas) for sizing and appearance.

## Demos and related pages

- [Custom nodes live demo](https://grafloria.com/demos/nodes/custom-nodes.html): select its Qwik binding to inspect framework-rendered cards.
- [Table / ER live demo](https://grafloria.com/demos/diagrams/table-er.html): inspect the shipped table-card kit and its relationships.
- [Dashboard builder live demo](https://grafloria.com/demos/dashboard/dashboard-builder.html): explore boards built from widget data.
- [Qwik gallery and source](https://grafloria.com/demos-qwik/): view the Qwik examples in place.
- [Qwik state and resumption](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/qwik-state-and-resumption): connect application controls to the live instance.
