# Theme a canvas

Use a theme for canvas-wide defaults and spec styles for individual nodes and edges. One [`Theme`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-types-interfaces-s-v) 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:

```bash
npm install @grafloria/element @grafloria/renderer @grafloria/engine
```

React:

```bash
npm install @grafloria/react @grafloria/element @grafloria/renderer @grafloria/engine react react-dom
```

Vue:

```bash
npm install @grafloria/vue @grafloria/element @grafloria/renderer @grafloria/engine vue
```

Angular:

```bash
npm install @grafloria/angular @grafloria/element @grafloria/renderer @grafloria/engine @angular/common @angular/core @angular/forms @angular/platform-browser rxjs
```

Qwik:

```bash
npm 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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-nodespec) and [`EdgeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-edgespec); the bindings turn these specs into live models.

Start with the shipped [`LIGHT_THEME`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-themes-constants), [`DARK_THEME`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-themes-constants), [`HIGH_CONTRAST_LIGHT_THEME`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-themes-constants) and [`HIGH_CONTRAST_DARK_THEME`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-themes-constants). Use [`themeRef`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-themes-functions) 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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-themes-functions). This is the named-style extension point; the mounted canvas consumes the definitions through `style.styleClass`.

The initialization helper receives a mounted [`DiagramInstance`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-diagraminstance). The shipped [`muiBridge`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-themes-functions) supplies the host-token map used by the buttons.

```ts title="canvas-theme.ts"
import {
  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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core), or bind it on React's [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react), Vue's [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-vue), Qwik's [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik) or Angular's [`DiagramCanvasComponent`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-diagramcanvascomponent); see [Edit nodes](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/edit-nodes) for mounting and instance access.

:::code-group
```ts title="JavaScript"
import { 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);
}
```
```tsx title="React"
import { 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 title="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>
```
```ts title="Angular"
import { 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(); }
}
```
```tsx title="Qwik"
import { 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>
  );
});
```
:::

![Initial light palette: four nodes, a dashed upper edge, a solid warning-colored lower edge, and six palette and token buttons. The lower-right node has a purple-to-pink fill and a shadow.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/303df64adb0278d9289f1367f61c7248.png)

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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-themes-types) when Grafloria selects the palette from a light/dark choice or OS preferences:

- React and Qwik: pass `colorMode="system"` to the flow instead of `theme`.
- Vue: pass `color-mode="system"` instead of `:theme`.
- Angular: bind `[colorMode]="'system'"` instead of `[theme]`.
- JavaScript: pass `colorMode: 'system'` in the options to `render()` instead of `theme`.

`'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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-themes-interfaces) 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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-themes-types): 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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-themes-functions) or [`tailwindBridge`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-themes-functions). 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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/javascript-elements-and-content).

## Live demos and related guides

- [Dark mode](https://grafloria.com/demos/styling/dark-mode.html) exercises mode and OS preferences.
- [Themes and design tokens](https://grafloria.com/demos/styling/themes-and-tokens.html) switches among the shipped token bridges.
- [Named style classes](https://grafloria.com/demos/styling/named-style-classes.html) demonstrates cascade precedence.
- [Theme-bound properties](https://grafloria.com/demos/styling/theme-bound-properties.html) switches semantic paint and numeric scales.
- [Paint servers](https://grafloria.com/demos/styling/paint-servers.html) shows gradients, patterns and shadows.
- [Instance and lifecycle](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/instance-and-lifecycle) covers ownership and teardown.
