Use outlines to show hover, selection and validation, and connected-line highlighting to follow a selected step. Use execution statuses and animated strokes to show progress. The examples draw a completed Fetch node, a pulsing Transform node, a pending Save node, and a moving token on the Fetch–Transform wire.
1. Define the progress display
Keep specs and live state separate: NodeSpec and EdgeSpec seed the diagram; query its live DiagramModel to update progress. The outline settings use HighlighterConfig; connected-line settings use HighlightConnectedOptions.
Create state.ts in your browser application's source directory, then mount it with the entry file in step 2. Run on its own, this module mounts a preview; the framework entry replaces that preview when it initializes. The extra Audit–Log branch makes the dimming visible: Transform starts selected, so its connected wires stand out while the unrelated wire fades.
typescriptimport { render } from '@grafloria/element';
import type { DiagramModel } from '@grafloria/engine';
import type {
DiagramInstance, NodeSpec, EdgeSpec,
HighlighterConfig, HighlightConnectedOptions,
} from '@grafloria/renderer';
export const nodes: NodeSpec[] = [
{ id: 'fetch', label: 'Fetch', position: { x: 40, y: 80 },
size: { width: 120, height: 56 } },
{ id: 'transform', label: 'Transform', selected: true,
position: { x: 280, y: 80 }, size: { width: 120, height: 56 } },
{ id: 'save', label: 'Save', position: { x: 520, y: 80 },
size: { width: 120, height: 56 } },
{ id: 'audit', label: 'Audit', position: { x: 40, y: 240 },
size: { width: 120, height: 56 } },
{ id: 'log', label: 'Log', position: { x: 280, y: 240 },
size: { width: 120, height: 56 } },
];
export const edges: EdgeSpec[] = [
{ id: 'active', source: 'fetch', target: 'transform',
style: { animation: { type: 'marching-ants', speed: 'normal' } } },
{ id: 'next', source: 'transform', target: 'save' },
{ id: 'other', source: 'audit', target: 'log' },
];
export const outlines: Partial<HighlighterConfig> = {
showValidation: false,
};
export const connected: HighlightConnectedOptions = {
depth: 1, outgoing: 'dashed', dimOpacity: 0.25,
};
let removePreview: (() => void) | undefined;
export function showProgress(model: DiagramModel): void {
removePreview?.();
removePreview = undefined;
model.getNode('fetch')?.setState({ status: 'completed' });
model.getNode('transform')?.setState({ status: 'running', animateStatus: true });
model.getNode('save')?.setState({ status: 'pending' });
}
export function attach(
instance: DiagramInstance,
reportDragging: (text: string) => void,
): () => void {
showProgress(instance.getModel());
instance.fitView(40);
instance.renderNow();
const removeRider = mountRider(instance.container);
const report = () => {
const ids = instance.getDraggingNodeIds();
reportDragging(ids.length ? ids.join(', ') : 'none');
};
report();
const timer = window.setInterval(report, 100);
return () => {
window.clearInterval(timer);
removeRider();
};
}
let riderId = 0;
export function mountRider(host: HTMLElement): () => void {
const painted = host.querySelector('svg');
if (!painted) return () => {};
const ns = 'http://www.w3.org/2000/svg';
const overlay = document.createElementNS(ns, 'svg');
overlay.style.cssText =
'position:absolute;inset:0;width:100%;height:100%;pointer-events:none';
overlay.setAttribute('aria-hidden', 'true');
const guide = document.createElementNS(ns, 'path');
do {
guide.id = `rider-${++riderId}`;
} while (document.getElementById(guide.id));
guide.setAttribute('fill', 'none');
guide.setAttribute('stroke', 'none');
const token = document.createElementNS(ns, 'circle');
token.setAttribute('r', '6');
token.setAttribute('fill', '#ec4899');
token.setAttribute('stroke', '#fff');
token.setAttribute('stroke-width', '1.5');
const motion = document.createElementNS(ns, 'animateMotion');
motion.setAttribute('dur', '2.4s');
motion.setAttribute('begin', '-0.6s');
motion.setAttribute('repeatCount', 'indefinite');
const path = document.createElementNS(ns, 'mpath');
path.setAttribute('href', `#${guide.id}`);
motion.appendChild(path);
token.appendChild(motion);
overlay.append(guide, token);
host.appendChild(overlay);
const sync = () => {
overlay.setAttribute('viewBox', painted.getAttribute('viewBox') ?? '0 0 680 360');
overlay.setAttribute('preserveAspectRatio',
painted.getAttribute('preserveAspectRatio') ?? 'xMidYMid meet');
const wire = painted.querySelector('[data-link-id="active"] path.diagram-link');
guide.setAttribute('d', wire?.getAttribute('d') ?? '');
};
sync();
const observer = new MutationObserver(sync);
observer.observe(painted, {
attributes: true, attributeFilter: ['d', 'viewBox', 'preserveAspectRatio'],
childList: true, subtree: true,
});
const preference = window.matchMedia('(prefers-reduced-motion: reduce)');
const respectMotion = () => { token.style.display = preference.matches ? 'none' : ''; };
respectMotion();
preference.addEventListener('change', respectMotion);
return () => {
observer.disconnect();
preference.removeEventListener('change', respectMotion);
overlay.remove();
};
}
if (typeof document !== 'undefined') {
const preview = document.createElement('div');
preview.style.cssText = 'position:relative;height:400px';
document.body.appendChild(preview);
const instance = render({ nodes, edges }, preview, {
highlighterConfig: outlines, highlightConnected: connected,
});
const detach = attach(instance, () => {});
removePreview = () => {
detach();
instance.dispose();
preview.remove();
};
}
The shipped stroke animation comes from style.animation. The token is different: it is application-owned SVG, not an engine particle API. Following the authors' animating-edges example, it rides a copy of the painted path with animateMotion and mpath. A sibling overlay mirrors the renderer's viewBox and path geometry during pan, zoom and rerouting. Do not append tokens inside the renderer-owned SVG subtree.
2. Mount it in your framework
Install the packages for your framework in your own project.
JavaScript:
bashnpm install @grafloria/element @grafloria/engine @grafloria/renderer
Angular:
bashnpm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/engine @grafloria/renderer rxjs @grafloria/element
Qwik:
bashnpm install @grafloria/qwik @grafloria/engine @grafloria/renderer @builder.io/qwik @grafloria/element
React:
bashnpm install @grafloria/react @grafloria/engine @grafloria/renderer react react-dom @grafloria/element
Vue:
bashnpm install @grafloria/vue @grafloria/engine @grafloria/renderer vue @grafloria/element
For this display, pass highlighterConfig and highlightConnected to render or React's GrafloriaFlow, Vue's GrafloriaFlow or Qwik's GrafloriaFlow, and use rendererConfig.highlightConnected on Angular's DiagramCanvasComponent; see edit nodes for mounting and accessing the DiagramInstance or Angular's activeEngine().
Use the matching file below alongside state.ts. Each mounts the same progress display in a sized container. In the instance-based bindings, the readout shows the ids currently being dragged; it reads none before movement passes the drag threshold and after release. The Angular canvas does not expose getDraggingNodeIds().
tsimport { render } from '@grafloria/element';
import { nodes, edges, outlines, connected, attach } from './state';
export function mountProgress(parent: HTMLElement): () => void {
const readout = document.createElement('p');
const host = document.createElement('div');
host.style.cssText = 'position:relative;height:400px';
parent.append(readout, host);
const instance = render({ nodes, edges }, host, {
highlighterConfig: outlines, highlightConnected: connected,
});
const detach = attach(instance, text => { readout.textContent = `Dragging: ${text}`; });
return () => {
detach();
instance.dispose();
readout.remove();
host.remove();
};
}
const parent = document.getElementById('app')!;
export const unmountProgress = mountProgress(parent);
// Call unmountProgress() when your application removes this view.
tsimport { AfterViewInit, Component, ElementRef, OnDestroy, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
import { nodes, edges, outlines, connected, showProgress, mountRider } from './state';
@Component({
selector: 'app-progress',
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<div #host style="position:relative;height:400px">
<grafloria-diagram-canvas
[(nodes)]="nodes" [(edges)]="edges"
[highlighterConfig]="outlines" [rendererConfig]="rendererConfig"
style="display:block;height:100%" />
</div>
`,
})
export class ProgressComponent implements AfterViewInit, OnDestroy {
readonly canvas = viewChild.required(DiagramCanvasComponent);
readonly host = viewChild.required<ElementRef<HTMLElement>>('host');
nodes: NodeSpec[] = nodes;
edges: EdgeSpec[] = edges;
readonly outlines = outlines;
readonly rendererConfig = { highlightConnected: connected };
private removeRider: (() => void) | undefined;
private frame: number | undefined;
ngAfterViewInit(): void {
const model = this.canvas().activeEngine()?.getDiagram();
if (model) showProgress(model);
this.canvas().fitToContent(40);
this.canvas().scheduleRender();
this.frame = requestAnimationFrame(() => {
this.removeRider = mountRider(this.host().nativeElement);
});
}
ngOnDestroy(): void {
if (this.frame !== undefined) cancelAnimationFrame(this.frame);
this.removeRider?.();
}
}
tsximport { component$, noSerialize, useSignal, useVisibleTask$, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import { nodes, edges, outlines, connected, attach } from './state';
export default component$(() => {
const dragging = useSignal('none');
const detach = useSignal<NoSerialize<() => void>>();
useVisibleTask$(({ cleanup }) => {
cleanup(() => detach.value?.());
});
return <>
<p>Dragging: {dragging.value}</p>
<div style={{ position: 'relative', height: '400px' }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
highlighterConfig={outlines} highlightConnected={connected}
onInit$={instance => {
detach.value?.();
detach.value = noSerialize(attach(instance, text => { dragging.value = text; }));
}} />
</div>
</>;
});
tsximport { useEffect, useRef, useState } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import { nodes, edges, outlines, connected, attach } from './state';
export default function Progress() {
const [dragging, setDragging] = useState('none');
const detach = useRef<(() => void) | undefined>(undefined);
useEffect(() => () => detach.current?.(), []);
return <>
<p>Dragging: {dragging}</p>
<div style={{ position: 'relative', height: 400 }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
highlighterConfig={outlines} highlightConnected={connected}
onInit={instance => {
detach.current?.();
detach.current = attach(instance, setDragging);
}} />
</div>
</>;
}
vue<script setup lang="ts"> import { onBeforeUnmount, ref } from 'vue'; import { GrafloriaFlow } from '@grafloria/vue'; import type { DiagramInstance } from '@grafloria/renderer'; import { nodes, edges, outlines, connected, attach } from './state'; const dragging = ref('none'); let detach: (() => void) | undefined; function init(instance: DiagramInstance): void { detach?.(); detach = attach(instance, text => { dragging.value = text; }); } onBeforeUnmount(() => detach?.()); </script> <template> <p>Dragging: {{ dragging }}</p> <div style="position:relative;height:400px"> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" :highlighter-config="outlines" :highlight-connected="connected" @init="init" /> </div> </template>
Fetch has a green completed stroke, Transform has a blue running stroke and pulse, and Save appears faded as pending. The pink circle travels along the animated incoming wire. Select another node to move the connected-line emphasis; click the background to remove it. Hover and selection outlines remain enabled, while validation outlines are disabled in this example.
3. Choose the effects you need
highlighterConfig={false} disables only the extra outline layer, not selection itself. true enables all kinds. An object starts from the defaults on each assignment, so { showHover: false } keeps the other kinds enabled rather than retaining a previous configuration.
| Option | Type | Default | What it does |
|---|---|---|---|
highlighterConfig | boolean | Partial<HighlighterConfig> | Off in the shared instance; true on Angular's canvas | Enables the extra outline layer. |
showHover, showSelection, showValidation, showConnectTargets | boolean | Each true when the layer is enabled | Selects which outline kinds appear. |
hoverPadding, selectionPadding, validationPadding | number | 2, 4, 6 | Adds padding in world coordinates. |
highlightConnected | boolean | HighlightConnectedOptions | Off | Highlights lines connected to selected nodes. |
depth | number | 1 | Follows incoming and outgoing paths by this many hops; Infinity traces the whole path. |
stroke | string | Theme's primary text colour | Colours highlighted lines. |
strokeWidth | number | 2.5 | Sets highlighted-line width. |
outgoing | 'solid' | 'dashed' | 'solid' | Adds an outgoing direction cue. |
dimOpacity | number | 0.4 | Fades unrelated lines while a node is selected; 1 leaves them undimmed. |
style.animation.type | 'marching-ants' | 'flow' | 'pulse' | 'dash-flow' | 'none' | No animation without an animation setting | Chooses a shipped edge-stroke animation, or turns it off. |
style.animation.speed | 'slow' | 'normal' | 'fast' | 'normal' | Changes stroke animation speed. |
style.animation.direction | 'forward' | 'reverse' | Forward unless set to 'reverse' | Reverses stroke animation. |
For a mounted instance, setHighlighterConfig() and setHighlightConnected() change these settings and schedule a repaint. getHighlightConnected() returns the current boolean or options object. To trace the whole path instead of immediate neighbours, pass { depth: Infinity } through the prop or setter.
Execution status belongs to the live NodeModel: call setState({ status }) with idle, pending, running, completed, error or warning. idle adds no status class; the others retain static visual cues even when motion is disabled. Set animateStatus: false to keep the status but suppress its motion. Change a live wire through LinkModel's updateStyle({ animation: { type: 'flow' } }), then request a repaint as the samples do.
The instance's animations property is its shipped AnimationService. setEnabled(false) disables motion globally, and updateConfig({ reducedMotion: true }) requests static rendering. System reduced-motion preferences are detected by default. The custom token is outside that service; this sample separately hides it for the system reduced-motion preference. Wire any application-level motion switch to your own overlay too.
Keep view effects separate from document changes
- Outlines and connected-line highlighting do not mutate the model or add undo steps. Dragging ids are a read-only observation of the active gesture, not a stored node property.
- Execution
statusandanimateStatusare durable node state.style.animationis link style, andupdateStyle()records a model change. These are not viewer-only effects; document read-only protection blocks those writes. - The samples directly set a progress snapshot; they do not execute a workflow or record commands on the history stack. Your application decides when a step runs, completes, warns or fails. Use commands and history for undoable editor actions, and save and restore documents for persistence.
- Keep the rider in its sibling overlay. Its observer copies routed geometry rather than assuming a straight line, and its teardown disconnects the observer when the view unmounts. The framework binding owns disposal of its diagram; the JavaScript host disposes its instance explicitly.
Live demos and related tasks
- Highlight connected lines: immediate neighbours, full-path tracing and lines lifted over crossing cards.
- Animating edges: all four stroke styles plus circles, packages and trains of tokens. View its source.
- Execute flow: application-driven status changes, failures, warnings and stepping.
- Turbo flow: animated borders and glowing gradient edges.
- Theme a canvas for appearance; route and label edges for geometry.
Was this page helpful?