Skip to content
D
Documentation

Style a diagram

how-to
3 min readUpdated

Use this page when the diagram's appearance belongs to the host application: start with a built-in theme, add per-node or per-edge styling, and connect the canvas to the application's design tokens. The examples render a small workflow, so you can see the fill, stroke, and theme changes on a mounted canvas.

Choose the styling layer

  • Use theme for the canvas-wide palette and defaults. LIGHT_THEME and DARK_THEME are shipped themes.
  • Use style.strokeWidth, style.fill, and style.stroke for one node or edge.
  • Use style.styleClass with defineStyle() for a reusable named style. Named styles follow the cascade theme < type-default < named-class < element-inline < state.
  • Use a token bridge when the host already exposes CSS variables. A bridge makes the diagram read the host's shadcn, MUI, or Tailwind variables instead of maintaining a second palette.

The host element must have a resolved height. A canvas inside a container with no height appears blank; give it 100vh, a flex size, or a sized grid cell.

JavaScript

Install the element and engine packages:

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

Mount a real spec with render, then keep the returned DiagramInstance for live changes. This sample starts dark, makes the middle node thicker, applies a named orange style to the first node, and connects the canvas to shadcn tokens.

html
<button id="toggle">Toggle theme</button>
<div id="diagram" style="height:400px"></div>
js
import { render, DARK_THEME, shadcnBridge, defineStyle } from '@grafloria/element';

const host = document.getElementById('diagram');
if (!host) throw new Error('Missing #diagram');
host.style.height = '400px';
defineStyle('warning-step', { fill: '#fed7aa', stroke: '#c2410c', strokeWidth: 2 });

const nodes = [
  { id: 'start', position: { x: 40, y: 80 }, size: { width: 170, height: 70 }, data: { label: 'Start' }, style: { styleClass: 'warning-step' } },
  { id: 'review', position: { x: 280, y: 80 }, size: { width: 170, height: 70 }, data: { label: 'Review' }, style: { strokeWidth: 5 } },
  { id: 'done', position: { x: 520, y: 80 }, size: { width: 170, height: 70 }, data: { label: 'Done' } },
];
const edges = [
  { id: 'start-review', source: 'start', target: 'review' },
  { id: 'review-done', source: 'review', target: 'done', style: { strokeWidth: 3 } },
];

const instance = render({ nodes, edges }, host, { theme: DARK_THEME });
instance.setTokenBridge(shadcnBridge());
instance.renderNow();

The host now contains three visible nodes and two links. The first node uses the named orange style, the second node has a 5px border, and the second link has a 3px stroke. setTokenBridge() changes the instance's palette without changing the node data; renderNow() repaints synchronously.

Use a built-in theme

Set the component's theme prop when the framework owns the diagram. The following snippets use the same nodes and edges in each framework; each host has a real height. The JavaScript example below shows a live theme swap through the instance.

The framework entry points are GrafloriaFlow for React, GrafloriaFlow for Vue, GrafloriaFlow for Qwik, and DiagramCanvasComponent for Angular.

tsx
import { GrafloriaFlow, DARK_THEME } from '@grafloria/react';
import type { NodeSpec, EdgeSpec } from '@grafloria/react';

const nodes: NodeSpec[] = [
  { id: 'a', position: { x: 60, y: 80 }, size: { width: 170, height: 70 }, data: { label: 'Order' } },
  { id: 'b', position: { x: 300, y: 80 }, size: { width: 170, height: 70 }, data: { label: 'Ship' } },
];
const edges: EdgeSpec[] = [{ id: 'e1', source: 'a', target: 'b' }];

export default function StyledFlow() {
  return (
    <div style={{ height: '100vh' }}>
      <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} theme={DARK_THEME} />
    </div>
  );
}

In Angular, DiagramCanvasComponent is the canvas element. In the other framework bindings, GrafloriaFlow is rendered as a component. The JavaScript example uses the instance's setTheme() method; retain the instance rather than recreating the canvas.

Add named styles and a token bridge

Register a named style once, then name it from a node's style.styleClass. Namespace names in applications because the style registry is process-wide. An inline property outranks the named class, so this node is green even though its class declares orange:

js
import { defineStyle } from '@grafloria/element';

defineStyle('review-step', { fill: '#fed7aa', stroke: '#c2410c', strokeWidth: 2 });
const node = {
  id: 'review',
  position: { x: 80, y: 80 },
  size: { width: 180, height: 72 },
  data: { label: 'Review' },
  style: { styleClass: 'review-step', fill: '#22c55e' },
};

For a host that uses CSS variables, import one of the shipped bridges and pass it to the live instance:

js
import { render, LIGHT_THEME, shadcnBridge, defineStyle } from '@grafloria/element';

const host = document.getElementById('diagram') ?? document.body.appendChild(document.createElement('div'));
host.id = 'diagram';
host.style.cssText = 'height:400px;width:800px;display:block';
defineStyle('review-step', { fill: '#fed7aa', stroke: '#c2410c', strokeWidth: 2 });
const node = {
  id: 'review',
  position: { x: 80, y: 80 },
  size: { width: 180, height: 72 },
  data: { label: 'Review' },
  style: { styleClass: 'review-step', fill: '#22c55e' },
};
const instance = render({ nodes: [node], edges: [] }, host, { theme: LIGHT_THEME });
instance.setTokenBridge(shadcnBridge());
instance.renderNow();

shadcnBridge(), muiBridge(), and tailwindBridge() map the corresponding host vocabulary. The bridge is instance-scoped: two diagrams can use different themes on one page without one instance overwriting the other's CSS variables. To change the host palette, update the host's variables or class, then repaint the instance.

Pitfalls

  • A host without a resolved height produces a blank canvas. Size the host or its flex/grid parent before mounting.
  • A named style does not beat an element-inline property. Put the property on the node when that node must win.
  • Do not use a Mermaid-style text string as the render() spec. render() mounts a data object; text import is a separate API.
  • Use ./create-custom-nodes.md when you need a custom node; omit custom: true and the renderer uses a stock rectangle. Register its renderer before mount.
  • Use ./build-er-and-uml-diagrams.md for custom-node update behavior; custom renderers run at mount, so update DOM you own or repaint the dashboard widget.

See also

Was this page helpful?