# Theme diagrams

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`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-types-interfaces-t-v) is
data: pass the built-in [`LIGHT_THEME`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-themes-constants) or
[`DARK_THEME`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-themes-constants), 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](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/edit-mermaid-diagrams) for that pitfall.

## JavaScript: mount two themed diagrams

Call [`render`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-element-core) with a spec, a host, and options. It returns a
[`DiagramInstance`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-diagraminstance), so you can switch
the theme and repaint the live diagram without rebuilding its data.

```js
import { 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.

:::code-group
```tsx title="React"
import { 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 title="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>
```
```ts title="Angular"
import { 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 } }];
}
```
```text title="Qwik"
import { 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](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/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](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/customize-nodes).

## Live demo

Try [Themes & design tokens](https://grafloria.com/demos/styling/themes-and-tokens.html)
to switch a mounted graph between the library theme and host design tokens. Its source is
the [themes-and-tokens demo](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/styling/themes-and-tokens.html).

## Related

- [How Grafloria works](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/how-grafloria-works)
- [The DiagramInstance](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-diagraminstance)
- [Custom nodes](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/customize-nodes)
