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 for setup.
bashnpm 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's NodeTypes map automatically opts matching nodes into HTML rendering unless they specify custom: false; see React: custom content for the shared type-to-component mapping.
The Qwik sample uses NodeProps with a ServiceData payload to carry owner and status across the container boundary, alongside typed NodeSpec and EdgeSpec inputs; see 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.
tsximport { 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' }}
/>
));
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 uses the same container boundary. Map a widget's kind through WidgetTypes, and receive its full spec and data through WidgetProps. Feed it through widget.data; do not rely on surrounding application contexts.
Declare the board with 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.
tsximport { 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 for the shared mount-once input behavior. For runtime widget edits, see Build a dashboard.
3. Build a kit through spec$ and remount for new data
Use GrafloriaDiagram for a kit rather than rebuilding its cards yourself. The shipped 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 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, and fitView(40) frames its content.
Type the schema data with ErEntitySpec and ErRelationshipSpec.
tsximport { 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>
);
});
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.
Options that matter
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 for sizing and appearance.
Demos and related pages
- Custom nodes live demo: select its Qwik binding to inspect framework-rendered cards.
- Table / ER live demo: inspect the shipped table-card kit and its relationships.
- Dashboard builder live demo: explore boards built from widget data.
- Qwik gallery and source: view the Qwik examples in place.
- Qwik state and resumption: connect application controls to the live instance.
Was this page helpful?