# Motion
URL: /diagrammatic/docs/motion

Animate flow direction and live points with one attribute and the shipped stylesheet, on charts that stay server components.

> For AI agents: a documentation index is available at [llms.txt](/llms.txt). Use `.md` for canonical markdown pages; `.mdx` is kept as a backwards-compatible alias on supported URL paths.

Charts that draw a direction or a live reading stamp motion layers into their SVG. The layers are invisible and inert until two things are true: `diagrammatic/styles.css` is loaded, and the chart or any ancestor carries `data-dg-motion`. Motion is CSS only, so the chart stays a server component and no JavaScript runs to animate it.

```
import "diagrammatic/styles.css";
import { Sankey } from "diagrammatic";

<div data-dg-motion="">
  <Sankey graph={graph} />
</div>;
```

```tsx
import { Sankey } from "diagrammatic";

export default function SignupFlowSankey() {
  return (
    <div data-dg-motion="">
      <Sankey
        title="Where this week's signups came from and went"
        labels={["source", "first action", "outcome"]}
        graph={{
          nodes: [
            { id: "search", group: 0 },
            { id: "docs", group: 0 },
            { id: "referral", group: 0 },
            { id: "template", group: 1 },
            { id: "blank", group: 1 },
            { id: "activated", group: 2 },
            { id: "dropped", group: 2 },
          ],
          links: [
            { source: "search", target: "template", value: 420 },
            { source: "search", target: "blank", value: 180 },
            { source: "docs", target: "template", value: 260 },
            { source: "docs", target: "blank", value: 140 },
            { source: "referral", target: "template", value: 120 },
            { source: "template", target: "activated", value: 610 },
            { source: "template", target: "dropped", value: 190 },
            { source: "blank", target: "activated", value: 110 },
            { source: "blank", target: "dropped", value: 210 },
          ],
        }}
      />
    </div>
  );
}
```

## What moves

| Layer      | Attribute                         | Charts                                                | What it shows                                                                                                                                                                                        |
| ---------- | --------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Flow trail | `data-dg-flow`                    | `Sankey`, `Chord`, `Network`, `ArcDiagram`, `FlowMap` | A comet travels each connection from its source to its target. In a `Sankey` one sweep crosses the columns in order: every ribbon in the first stage runs together, then hands off to the next stage |
| Flow step  | `data-dg-flow-step` with `--dg-t` | `DotWorldMap`                                         | The symbols along each trail light up in order, source first                                                                                                                                         |
| Pulse      | `data-dg-pulse`                   | `Line` with one series, `Sparkline`                   | A halo pulses from the latest point, for a live reading                                                                                                                                              |

Motion only explains something the still figure cannot: which way a connection runs, or that a value is live. Charts without either stay still.

```tsx
import { DotWorldMap } from "diagrammatic/world";

export default function LiveTrafficMap() {
  return (
    <div data-dg-motion="">
      <DotWorldMap
        title="Traffic failing over between regions, live"
        groups={{
          "us-east": ["US", "CA", "MX", "BR"],
          "eu-west": ["GB", "FR", "DE", "ES", "IT"],
          "ap-south": ["IN", "SG", "JP", "AU"],
        }}
        flows={[
          { from: "BR", to: "US" },
          { from: "GB", to: "US" },
          { from: "IN", to: "DE" },
          { from: "AU", to: "SG" },
          { from: "JP", to: "US" },
          { from: "DE", to: "SG" },
        ]}
      />
    </div>
  );
}
```

## Live readings

Toggle the attribute with the data source, so the pulse means the reading is live.

```tsx
"use client";

import { useEffect, useState, useSyncExternalStore } from "react";
import { Line } from "diagrammatic";

const WINDOW = 30;
const REDUCED = "(prefers-reduced-motion: reduce)";

function subscribe(onChange: () => void) {
  const query = window.matchMedia(REDUCED);
  query.addEventListener("change", onChange);
  return () => query.removeEventListener("change", onChange);
}

function next(prev: number[], t: number): number[] {
  const last = prev[prev.length - 1] ?? 40;
  const noise = Math.abs(Math.sin(t * 12.9898) * 43758.5453) % 1;
  const drift = Math.sin(t / 6) * 3 + noise * 4 - 2;
  return [...prev.slice(1), Math.max(5, Math.min(95, last + drift))];
}

export default function LiveCpuChart() {
  const [data, setData] = useState(() =>
    Array.from({ length: WINDOW }, (_, i) => 40 + Math.sin(i / 4) * 8),
  );
  const reduced = useSyncExternalStore(
    subscribe,
    () => window.matchMedia(REDUCED).matches,
    () => false,
  );
  const [choice, setChoice] = useState<boolean | null>(null);
  const running = choice ?? !reduced;

  useEffect(() => {
    if (!running) return;
    let t = 0;
    const id = setInterval(() => {
      t += 1;
      setData((prev) => next(prev, t));
    }, 1000);
    return () => clearInterval(id);
  }, [running]);

  return (
    <div data-dg-motion={running ? "" : undefined}>
      <Line
        title="CPU, last 30 seconds"
        data={data}
        yMax={100}
        yTicks={[
          { at: 0, label: "0%" },
          { at: 50, label: "50%" },
          { at: 100, label: "100%" },
        ]}
        format={(v) => `${Math.round(v)}%`}
      />
      <button
        type="button"
        onClick={() => setChoice(!running)}
        className="text-muted-foreground hover:text-foreground mt-3 text-xs"
      >
        {running ? "Pause" : "Resume"}
      </button>
    </div>
  );
}
```

## Reduced motion

Every motion rule sits inside `@media (prefers-reduced-motion: no-preference)`. A reader who asks for reduced motion sees the still figure, with nothing for you to check.

## Timing and style

`--dg-motion-duration` sets how long one pass takes (2.4s by default). Every trail in a figure shares that one pass; each trail carries `--dg-flow-from` and `--dg-flow-to`, the offsets that place its comet in the sweep, so leave its dash pattern to the stylesheet and restyle color, width, or opacity instead.

```
[data-dg-motion] {
  --dg-motion-duration: 4s;
}
[data-dg-motion] [data-dg-flow] {
  stroke-opacity: 0.6;
}
```

A highlighted chart hides the trails of muted connections and keeps them in step with the sweep, so a highlight reads the same in motion as at rest.

## Without the stylesheet

The layers draw nothing on their own: a trail has `stroke-opacity="0"` and a pulse has `opacity="0"`. A host that skips `styles.css` can write its own keyframes against the same attributes, or leave the charts still.