Use a theme for canvas-wide defaults and spec styles for individual nodes and edges. One Theme object drives node defaults, link colors and selection states; changing it repaints the mounted diagram without replacing your data.
The examples below draw four nodes: a theme-default node, an orange named-style node, a theme-bound warning node, and a gradient-filled node with a shadow. The buttons switch palettes or request a host-token bridge.
1. Install your binding
Run the command for your existing framework project. The JavaScript example runs in the browser through your project's bundler.
JavaScript:
bashnpm install @grafloria/element @grafloria/renderer @grafloria/engine
React:
bashnpm install @grafloria/react @grafloria/element @grafloria/renderer @grafloria/engine react react-dom
Vue:
bashnpm install @grafloria/vue @grafloria/element @grafloria/renderer @grafloria/engine vue
Angular:
bashnpm install @grafloria/angular @grafloria/element @grafloria/renderer @grafloria/engine @angular/common @angular/core @angular/forms @angular/platform-browser rxjs
Qwik:
bashnpm install @grafloria/qwik @grafloria/element @grafloria/renderer @grafloria/engine @builder.io/qwik
2. Describe the paint once
Create canvas-theme.ts beside your component. Type the data with NodeSpec and EdgeSpec; the bindings turn these specs into live models.
Start with the shipped LIGHT_THEME, DARK_THEME, HIGH_CONTRAST_LIGHT_THEME and HIGH_CONTRAST_DARK_THEME. Use themeRef when a property expresses a meaning rather than a fixed color: category.warning reads the active theme's warning palette, and numbers.emphasis reads its numeric scale.
Register reusable paint with defineStyles. This is the named-style extension point; the mounted canvas consumes the definitions through style.styleClass.
The initialization helper receives a mounted DiagramInstance. The shipped muiBridge supplies the host-token map used by the buttons.
tsimport {
LIGHT_THEME, DARK_THEME,
HIGH_CONTRAST_LIGHT_THEME, HIGH_CONTRAST_DARK_THEME,
defineStyles, themeRef, muiBridge,
type Theme, type NodeSpec, type EdgeSpec, type DiagramInstance,
} from '@grafloria/renderer';
export const palettes: Record<string, Theme> = {
light: LIGHT_THEME,
dark: DARK_THEME,
contrastLight: HIGH_CONTRAST_LIGHT_THEME,
contrastDark: HIGH_CONTRAST_DARK_THEME,
};
export function registerPaint(): void {
defineStyles({
'orders-warning': { fill: '#fed7aa', stroke: '#9a3412', strokeWidth: 2 },
'orders-bold': { strokeWidth: 5 },
});
}
export const nodes: NodeSpec[] = [
{ id: 'plain', label: 'Theme default',
position: { x: 50, y: 70 }, size: { width: 180, height: 76 } },
{ id: 'named', label: 'Named warning',
position: { x: 350, y: 70 }, size: { width: 180, height: 76 },
style: { styleClass: 'orders-warning orders-bold' } },
{ id: 'bound', label: 'Theme-bound warning',
position: { x: 50, y: 230 }, size: { width: 180, height: 76 },
style: {
fill: themeRef('category.warning'),
stroke: themeRef('category.warning'),
strokeWidth: themeRef('numbers.emphasis'),
} },
{ id: 'gradient', label: 'Gradient + shadow',
position: { x: 350, y: 230 }, size: { width: 180, height: 76 },
style: {
fill: {
type: 'linear', x1: 0, y1: 0, x2: 1, y2: 0,
stops: [
{ offset: 0, color: '#ddd6fe' },
{ offset: 1, color: '#fbcfe8' },
],
},
stroke: '#7c3aed', strokeWidth: 2,
shadow: { offsetX: 4, offsetY: 6, blur: 8, color: 'rgba(0,0,0,0.3)' },
} },
];
export const edges: EdgeSpec[] = [
{ id: 'named-edge', source: 'plain', target: 'named',
style: { styleClass: 'orders-warning', strokeDasharray: '6 3' } },
{ id: 'bound-edge', source: 'bound', target: 'gradient',
style: {
stroke: themeRef('category.warning'),
strokeWidth: themeRef('numbers.emphasis'),
} },
];
export function prepareCanvas(instance: DiagramInstance): void {
registerPaint();
instance.renderNow();
}
export const hostBridge = muiBridge();
export function useHostTokens(
instance: DiagramInstance | null | undefined,
enabled: boolean,
): void {
if (!instance) return;
instance.setTokenBridge(enabled ? hostBridge : null);
instance.renderNow();
const node = instance.container.querySelector('[data-node-id="plain"] rect.diagram-node');
if (!node) return;
const paint = getComputedStyle(node);
let readout = instance.container.querySelector('output');
if (!readout) {
readout = document.createElement('output');
readout.style.cssText = 'position:absolute; bottom:0; left:0; background:white; color:black';
instance.container.append(readout);
}
readout.textContent = `Resolved default paint: fill ${paint.fill}; stroke ${paint.stroke}`;
}
export const hostCSS = `
.orders-host {
--mui-palette-background-paper: #fffbf2;
--mui-palette-divider: #b8860b;
--mui-palette-text-primary: #1c1b1f;
--mui-palette-primary-main: #6750a4;
}
`;
The named node gets the orange fill and a five-unit border: later names in the space-separated styleClass list win conflicts. An element's own fill, stroke or strokeWidth overrides its named styles; interaction state sits above both. The cascade is theme → type-default → named-class → element-inline → state.
Gradient objects produce SVG paint servers, and shadow objects produce drop-shadow filters. Use a linear gradient's normalized endpoints and stops as above, or a radial gradient with type: 'radial', cx, cy, r and stops. Edge stroke accepts the same gradient objects; an edge is a line, not a filled box.
3. Mount and switch the palette
Choose your framework tab. Each sample uses the shared file from step 2 and gives the drawing a resolved height of 400px. Light, Dark, Contrast light and Contrast dark change the theme. Host tokens requests the MUI bridge; Theme tokens requests its removal.
Pass theme to JavaScript's render, or bind it on React's GrafloriaFlow, Vue's GrafloriaFlow, Qwik's GrafloriaFlow or Angular's DiagramCanvasComponent; see Edit nodes for mounting and instance access.
tsimport { render } from '@grafloria/element';
import {
nodes, edges, palettes, prepareCanvas, useHostTokens, hostCSS,
} from './canvas-theme';
const wrapper = document.createElement('section');
wrapper.className = 'orders-host';
const style = document.createElement('style');
style.textContent = hostCSS;
const toolbar = document.createElement('div');
const canvas = document.createElement('div');
canvas.style.height = '400px';
wrapper.append(style, toolbar, canvas);
document.body.append(wrapper);
const instance = render({ nodes, edges }, canvas, { theme: palettes.light });
prepareCanvas(instance);
for (const [key, label] of [
['light', 'Light'], ['dark', 'Dark'],
['contrastLight', 'Contrast light'], ['contrastDark', 'Contrast dark'],
]) {
const button = document.createElement('button');
button.textContent = label;
button.onclick = () => instance.setTheme(palettes[key]);
toolbar.append(button);
}
for (const enabled of [true, false]) {
const button = document.createElement('button');
button.textContent = enabled ? 'Host tokens' : 'Theme tokens';
button.onclick = () => useHostTokens(instance, enabled);
toolbar.append(button);
}
tsximport { useRef, useState } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import type { DiagramInstance, Theme } from '@grafloria/renderer';
import {
nodes, edges, palettes, prepareCanvas, useHostTokens, hostCSS,
} from './canvas-theme';
export default function App() {
const [theme, setTheme] = useState<Theme>(palettes.light);
const instance = useRef<DiagramInstance | null>(null);
return (
<section className="orders-host">
<style>{hostCSS}</style>
<button onClick={() => setTheme(palettes.light)}>Light</button>
<button onClick={() => setTheme(palettes.dark)}>Dark</button>
<button onClick={() => setTheme(palettes.contrastLight)}>Contrast light</button>
<button onClick={() => setTheme(palettes.contrastDark)}>Contrast dark</button>
<button onClick={() => useHostTokens(instance.current, true)}>Host tokens</button>
<button onClick={() => useHostTokens(instance.current, false)}>Theme tokens</button>
<div style={{ height: 400 }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} theme={theme}
onInit={(api) => { instance.current = api; prepareCanvas(api); }} />
</div>
</section>
);
}
vue<script setup lang="ts"> import { shallowRef } from 'vue'; import { GrafloriaFlow } from '@grafloria/vue'; import type { DiagramInstance, Theme } from '@grafloria/renderer'; import { nodes, edges, palettes, prepareCanvas, useHostTokens, hostCSS, } from './canvas-theme'; const theme = shallowRef<Theme>(palettes.light); const instance = shallowRef<DiagramInstance | null>(null); function onInit(api: DiagramInstance): void { instance.value = api; prepareCanvas(api); } </script> <template> <section class="orders-host"> <component :is="'style'">{{ hostCSS }}</component> <button @click="theme = palettes.light">Light</button> <button @click="theme = palettes.dark">Dark</button> <button @click="theme = palettes.contrastLight">Contrast light</button> <button @click="theme = palettes.contrastDark">Contrast dark</button> <button @click="useHostTokens(instance, true)">Host tokens</button> <button @click="useHostTokens(instance, false)">Theme tokens</button> <div style="height:400px"> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" :theme="theme" @init="onInit" /> </div> </section> </template>
tsimport { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { Theme, TokenBridge, NodeSpec, EdgeSpec } from '@grafloria/renderer';
import {
nodes, edges, palettes, registerPaint, hostBridge, hostCSS,
} from './canvas-theme';
@Component({
selector: 'app-root',
standalone: true,
imports: [DiagramCanvasComponent],
styles: [hostCSS],
template: `
<section class="orders-host">
<button (click)="theme = palettes['light']">Light</button>
<button (click)="theme = palettes['dark']">Dark</button>
<button (click)="theme = palettes['contrastLight']">Contrast light</button>
<button (click)="theme = palettes['contrastDark']">Contrast dark</button>
<button (click)="bridge = hostBridge">Host tokens</button>
<button (click)="bridge = undefined">Theme tokens</button>
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
[theme]="theme" [tokenBridge]="bridge" style="display:block; height:400px" />
</section>
`,
})
export class AppComponent {
readonly palettes = palettes;
readonly hostBridge = hostBridge;
theme: Theme = palettes['light'];
bridge: TokenBridge | undefined;
nodes: NodeSpec[] = nodes;
edges: EdgeSpec[] = edges;
constructor() { registerPaint(); }
}
tsximport { component$, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import type { DiagramInstance, Theme } from '@grafloria/renderer';
import {
nodes, edges, palettes, prepareCanvas, useHostTokens, hostCSS,
} from './canvas-theme';
export default component$(() => {
const theme = useSignal<Theme>(palettes.light);
const instance = useSignal<NoSerialize<DiagramInstance>>();
return (
<section class="orders-host">
<style dangerouslySetInnerHTML={hostCSS} />
<button onClick$={() => { theme.value = palettes.light; }}>Light</button>
<button onClick$={() => { theme.value = palettes.dark; }}>Dark</button>
<button onClick$={() => { theme.value = palettes.contrastLight; }}>Contrast light</button>
<button onClick$={() => { theme.value = palettes.contrastDark; }}>Contrast dark</button>
<button onClick$={() => useHostTokens(instance.value, true)}>Host tokens</button>
<button onClick$={() => useHostTokens(instance.value, false)}>Theme tokens</button>
<div style={{ height: '400px' }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} theme={theme.value}
onInit$={(api) => {
instance.value = noSerialize(api);
prepareCanvas(api);
}} />
</div>
</section>
);
});
The default node follows each palette. The literal orange and gradient fills remain literal; the warning node and its edge follow the theme's semantic colors and numeric scale. Qwik registers the named styles in the browser's initialization callback and keeps the live instance out of serialized state.
Choose theme or colorMode
Use theme when your application selects a complete palette, as above. Use ColorMode when Grafloria selects the palette from a light/dark choice or OS preferences:
- React and Qwik: pass
colorMode="system"to the flow instead oftheme. - Vue: pass
color-mode="system"instead of:theme. - Angular: bind
[colorMode]="'system'"instead of[theme]. - JavaScript: pass
colorMode: 'system'in the options torender()instead oftheme.
'light' and 'dark' select the light/dark axis explicitly; 'system' follows prefers-color-scheme. A request for more contrast or active forced colors upgrades that axis to the available high-contrast theme, even with an explicit light/dark choice. The default set includes both high-contrast palettes.
On a mounted instance, setColorMode('system') starts following the OS and getColorMode() returns the requested mode, not the resolved theme. Pass a ThemeSet as the second argument to choose your own light, dark, highContrastLight and highContrastDark palettes. Angular exposes the same set through [themes].
While a binding's colorMode is set, it takes precedence over theme. In React, removing the prop keeps the last mode. Do not combine the manual palette buttons above with a mode prop that selects a different palette.
Bridge and scope CSS variables
The shared file uses the shipped muiBridge to map Grafloria colors to MUI's --mui-palette-* variables. Host tokens calls setTokenBridge(hostBridge); Theme tokens calls setTokenBridge(null). Angular supplies or removes the bridge through its [tokenBridge] input.
In JavaScript, React, Vue and Qwik, the token-button helper reads the default node's browser-computed fill and stroke when it finds the rendered rectangle. The readout reports computed paint, not the requested palette; a bridge request alone does not establish a visible paint change.
The bridge is a TokenBridge: a map from a token such as node.fill to a CSS expression such as var(--app-card). For other design systems, use the shipped shadcnBridge or tailwindBridge. The shadcn preset supports HSL components, OKLCH components or full color values; the Tailwind preset targets v4's --color-* variables.
Keep host variables on the wrapper, as .orders-host does, rather than changing :root for one diagram. Grafloria scopes its own variable block to the rendered root's data-grafloria-instance value. Two diagrams can use different themes on one page; setTheme() rewrites only the target instance's block. No Grafloria stylesheet import is required.
styleClass names registered paint; it is not a CSS class selector. Use style.className for a host-CSS hook on a rendered node or edge. Keep that CSS under your wrapper selector too.
Options that matter
| Option | Type | Default | What it does |
|---|---|---|---|
theme | Theme | Built-in light theme without another theme source | Sets the palette for defaults and state colors. |
colorMode | 'light' | 'dark' | 'system' | No requested mode | Selects the light/dark axis and watches accessibility preferences. |
Angular themes / setColorMode() second argument | ThemeSet | Built-in light, dark and high-contrast set | Supplies the palettes a mode chooses between. |
tokenBridge | TokenBridge | No bridge | Repoints scoped variables to host CSS values. |
style.styleClass | string | No named style | Applies registered styles, left to right. |
style.className | string | No extra class | Adds a hook for your CSS. |
Node style.fill / node or edge style.stroke | Color string, linear/radial gradient or pattern | Cascade supplies paint | Overrides lower paint layers. |
style.strokeWidth | number | Cascade supplies width | Sets border or line weight. |
Node style.shadow | Boolean or shadow object | No element override | Uses a drop-shadow filter for a shadow object. |
Pitfalls
A canvas fills its parent. If that parent has no resolved height, the drawing is blank: give the wrapper a real height or a sized flex/grid allocation, as the samples do.
Namespace global named styles, such as orders-warning, to avoid replacing another feature's definitions. Fixed colors do not become theme-bound when you switch palettes; use themeRef() for paint that must follow a theme.
For styling your own HTML node content, continue with JavaScript elements and content.
Live demos and related guides
- Dark mode exercises mode and OS preferences.
- Themes and design tokens switches among the shipped token bridges.
- Named style classes demonstrates cascade precedence.
- Theme-bound properties switches semantic paint and numeric scales.
- Paint servers shows gradients, patterns and shadows.
- Instance and lifecycle covers ownership and teardown.
Was this page helpful?