Skip to content
D
Documentation

Highlight and animate state

how-to
4 min readUpdated

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.

typescript
import { 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:

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

Angular:

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

Qwik:

bash
npm install @grafloria/qwik @grafloria/engine @grafloria/renderer @builder.io/qwik @grafloria/element

React:

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

Vue:

bash
npm 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().

ts
import { 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.
Fetch has a green border, Transform is selected with blue outlines, and a pink token sits on their dashed wire. The Audit–Log wire is faded; the readout shows Dragging: none.

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.

OptionTypeDefaultWhat it does
highlighterConfigboolean | Partial<HighlighterConfig>Off in the shared instance; true on Angular's canvasEnables the extra outline layer.
showHover, showSelection, showValidation, showConnectTargetsbooleanEach true when the layer is enabledSelects which outline kinds appear.
hoverPadding, selectionPadding, validationPaddingnumber2, 4, 6Adds padding in world coordinates.
highlightConnectedboolean | HighlightConnectedOptionsOffHighlights lines connected to selected nodes.
depthnumber1Follows incoming and outgoing paths by this many hops; Infinity traces the whole path.
strokestringTheme's primary text colourColours highlighted lines.
strokeWidthnumber2.5Sets highlighted-line width.
outgoing'solid' | 'dashed''solid'Adds an outgoing direction cue.
dimOpacitynumber0.4Fades 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 settingChooses 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 status and animateStatus are durable node state. style.animation is link style, and updateStyle() 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.

Was this page helpful?