# Highlight and animate state

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`](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) seed the diagram; query its live [`DiagramModel`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-models-diagrammodel) to update progress. The outline settings use [`HighlighterConfig`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-interaction-interfaces-a-t); connected-line settings use [`HighlightConnectedOptions`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-types-interfaces-b-s).

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 title="state.ts"
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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core) or 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) or Qwik's [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik), and use `rendererConfig.highlightConnected` on 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 accessing the [`DiagramInstance`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-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()`.

:::code-group
```ts title="JavaScript"
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.
```
```ts title="Angular"
import { 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?.();
  }
}
```
```tsx title="Qwik"
import { 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>
  </>;
});
```
```tsx title="React"
import { 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 title="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 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.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/e55985a8fdadbd07c93514c341076080.png)

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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-models-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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-models-linkmodel)'s `updateStyle({ animation: { type: 'flow' } })`, then request a repaint as the samples do.

The instance's `animations` property is its shipped [`AnimationService`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-services). `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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/commands-and-history) for undoable editor actions, and [save and restore documents](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/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](https://grafloria.com/demos/edges/highlight-connected.html): immediate neighbours, full-path tracing and lines lifted over crossing cards.
- [Animating edges](https://grafloria.com/demos/edges/animating-edges.html): all four stroke styles plus circles, packages and trains of tokens. [View its source](https://github.com/grafloria/grafloria/blob/cccb505e76c7af26c8bafecaba8146db7d8df928/demos/edges/animating-edges.html).
- [Execute flow](https://grafloria.com/demos/interaction/execute-flow.html): application-driven status changes, failures, warnings and stepping.
- [Turbo flow](https://grafloria.com/demos/styling/turbo-flow.html): animated borders and glowing gradient edges.
- [Theme a canvas](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/theme-a-canvas) for appearance; [route and label edges](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/route-and-label-edges) for geometry.
