Use a theme for shared diagram styling, put an exception such as strokeWidth on the
node or edge spec, and keep each mounted diagram's theme in its own instance. This page
covers JavaScript, Angular, React, and Vue.
When to use a theme
Use a theme when several diagrams share palette, typography, spacing, effects, and default
node, link, or port styling. A Theme is
data: pass the built-in LIGHT_THEME or
DARK_THEME, or derive a theme by copying one
and changing its identity or supported values. Keep a one-off visual difference in the
spec instead of making a second global theme.
The examples below use four nodes and three links, so the result is a visible flow rather than an empty canvas. Give the host a height; without one there is no area in which to paint. See Edit Mermaid diagrams for that pitfall.
JavaScript: mount two themed diagrams
Call render with a spec, a host, and options. It returns a
DiagramInstance, so you can switch
the theme and repaint the live diagram without rebuilding its data.
jsimport { render } from '@grafloria/element';
import { DARK_THEME, LIGHT_THEME } from '@grafloria/renderer';
const spec = () => ({
nodes: [
{ id: 'order', position: { x: 60, y: 60 }, size: { width: 160, height: 70 }, data: { label: 'Order' } },
{ id: 'payment', position: { x: 300, y: 60 }, size: { width: 160, height: 70 }, data: { label: 'Payment' } },
{ id: 'fulfil', position: { x: 540, y: 60 }, size: { width: 160, height: 70 }, data: { label: 'Fulfil' } },
{ id: 'refund', position: { x: 300, y: 190 }, size: { width: 160, height: 70 }, data: { label: 'Refund' } },
],
edges: [
{ id: 'to-payment', source: 'order', target: 'payment', style: { strokeWidth: 3 } },
{ id: 'to-fulfil', source: 'payment', target: 'fulfil' },
{ id: 'to-refund', source: 'payment', target: 'refund' },
],
});
const host = (id) => {
const existing = document.getElementById(id);
if (existing instanceof HTMLElement) return existing;
const created = document.createElement('div');
created.id = id;
document.body.appendChild(created);
return created;
};
const lightHost = host('light');
const darkHost = host('dark');
lightHost.style.height = '300px';
darkHost.style.height = '300px';
const light = render(spec(), lightHost, { theme: LIGHT_THEME });
const dark = render(spec(), darkHost, { theme: DARK_THEME });
const derivedDark = { ...DARK_THEME, name: 'Acme dark' };
dark.setTheme(derivedDark);
dark.renderNow();
// The light diagram remains light; the dark diagram changes independently.
light.renderNow();
html<div id="light" style="height: 300px"></div>
<div id="dark" style="height: 300px"></div>
The page shows the same graph twice, with the first diagram light and the second dark.
The first link is visibly thicker because its spec sets strokeWidth: 3. Calling
setTheme() changes only the instance on which you call it. The renderer stamps each
root with an instance-specific scope for its injected variables and style element, so
the two diagrams do not overwrite one another.
The same pattern in each framework
Each binding mounts the component, supplies graph data typed with the library's types, and keeps the host tall enough to render. React and Vue store the instance from their init callback. Angular changes its bound theme value directly from the button.
tsximport { useRef, useState } from 'react';
import { GrafloriaFlow, DARK_THEME, LIGHT_THEME } from '@grafloria/react';
import type { DiagramInstance } from '@grafloria/react';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
const nodes: NodeSpec[] = [
{ id: 'a', position: { x: 40, y: 60 }, size: { width: 150, height: 70 }, data: { label: 'Order' } },
{ id: 'b', position: { x: 280, y: 60 }, size: { width: 150, height: 70 }, data: { label: 'Payment' } },
];
const edges: EdgeSpec[] = [{ id: 'e', source: 'a', target: 'b', style: { strokeWidth: 3 } }];
export default function ThemedDiagrams() {
const instance = useRef<DiagramInstance | null>(null);
const [dark, setDark] = useState(false);
const switchTheme = () => {
const next = !dark;
setDark(next);
instance.current?.setTheme(next ? DARK_THEME : LIGHT_THEME);
instance.current?.renderNow();
};
return <div style={{ height: 300 }}>
<button onClick={switchTheme}>Switch theme</button>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
theme={dark ? DARK_THEME : LIGHT_THEME}
onInit={(api) => { instance.current = api; }}
style={{ display: 'block', height: '260px' }} />
</div>;
}
vue<script setup lang="ts"> import { computed, ref } from 'vue'; import { GrafloriaFlow, DARK_THEME, LIGHT_THEME } from '@grafloria/vue'; import type { DiagramInstance } from '@grafloria/vue'; import type { NodeSpec, EdgeSpec } from '@grafloria/renderer'; const dark = ref(false); const theme = computed(() => dark.value ? DARK_THEME : LIGHT_THEME); const nodes: NodeSpec[] = [ { id: 'a', position: { x: 40, y: 60 }, size: { width: 150, height: 70 }, data: { label: 'Order' } }, { id: 'b', position: { x: 280, y: 60 }, size: { width: 150, height: 70 }, data: { label: 'Payment' } }, ]; const edges: EdgeSpec[] = [{ id: 'e', source: 'a', target: 'b', style: { strokeWidth: 3 } }]; let instance: DiagramInstance | null = null; function onInit(api: DiagramInstance) { instance = api; } </script> <template> <div style="height:300px"><button @click="dark = !dark">Switch theme</button> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" :theme="theme" @init="onInit" style="display:block;height:260px" /> </div> </template>
tsimport { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { DARK_THEME, LIGHT_THEME } from '@grafloria/renderer';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
@Component({
standalone: true,
imports: [DiagramCanvasComponent],
template: `<div style="height:300px"><button (click)="dark = !dark">Switch theme</button><grafloria-diagram-canvas
[nodes]="nodes" [edges]="edges" [theme]="dark ? darkTheme : lightTheme"
style="display:block;height:100%" /></div>`,
})
export class ThemedDiagramsComponent {
dark = false;
lightTheme = LIGHT_THEME;
darkTheme = DARK_THEME;
nodes: NodeSpec[] = [
{ id: 'a', position: { x: 40, y: 60 }, size: { width: 150, height: 70 }, data: { label: 'Order' } },
{ id: 'b', position: { x: 280, y: 60 }, size: { width: 150, height: 70 }, data: { label: 'Payment' } },
];
edges: EdgeSpec[] = [{ id: 'e', source: 'a', target: 'b', style: { strokeWidth: 3 } }];
}
textimport { component$, $, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik'; import { GrafloriaFlow, DARK_THEME, LIGHT_THEME, type DiagramInstance } from '@grafloria/qwik'; import type { NodeSpec, EdgeSpec } from '@grafloria/renderer'; const nodes: NodeSpec[] = [ { id: 'a', position: { x: 40, y: 60 }, size: { width: 150, height: 70 }, data: { label: 'Order' } }, { id: 'b', position: { x: 280, y: 60 }, size: { width: 150, height: 70 }, data: { label: 'Payment' } }, ]; const edges: EdgeSpec[] = [{ id: 'e', source: 'a', target: 'b', style: { strokeWidth: 3 } }]; export default component$(() => { const dark = useSignal(false); const instance = useSignal<NoSerialize<DiagramInstance>>(); const switchTheme = $(() => { dark.value = !dark.value; instance.value?.setTheme(dark.value ? DARK_THEME : LIGHT_THEME); instance.value?.renderNow(); }); return <div style={{ height: '300px' }}> <button onClick$={switchTheme}>Switch theme</button> <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} theme={dark.value ? DARK_THEME : LIGHT_THEME} style={{ display: 'block', height: '260px' }} onInit$={$((api: DiagramInstance) => { instance.value = noSerialize(api); })} /> </div>; });
In every tab, the button changes the rendered palette while the two-node graph remains in place, and the edge remains three pixels wide. In React and Vue, retain the initialized instance when later code needs an imperative call. In Angular, the button changes the bound theme property.
Options that matter
| Option | Type | Default | What it does |
|---|---|---|---|
theme | Theme | LIGHT_THEME | Supplies the palette and defaults for the mounted diagram. |
colorMode | 'light' | 'dark' | 'system' | not stated | Selects a pinned colour mode or follows the OS when set to 'system'. |
themes | ThemeSet | not stated | Supplies the theme set used with colour-mode resolution. |
strokeWidth | number in node/link style | not stated | Sets the painted stroke width for that styled node or link. |
setTheme(theme) performs a live theme swap and schedules a repaint. It forwards the
theme to the renderer and schedules the instance for painting. Use renderNow() when code must
measure the updated DOM immediately; otherwise the scheduled repaint is sufficient.
Pitfalls
- A host with no resolved height renders blank. Give it a fixed height, a flex size, or
100vh; see Edit Mermaid diagrams. render()accepts a data spec, not Mermaid text. Use the text import page for the DSL.- Theme changes are instance operations. Do not store a mutable global “current theme” and
expect existing diagrams to coordinate; call
setTheme()on each instance that should change. - Custom nodes need the opt-in described in Customize nodes.
Live demo
Try Themes & design tokens to switch a mounted graph between the library theme and host design tokens. Its source is the themes-and-tokens demo.
Was this page helpful?