Beck
Docs /Animate the flow

Animate the flow

This guide shows you how to choreograph a diagram's animation — packets, bursts, status pills, and persistent effects — so the motion tells a story rather than just decorating the page.

Flow works the same across every diagram type. A packet rides a connector; a step like status or fail marks a box. Wherever a step below takes a node, that means any box — an architecture node, a state, a sequence participant, or a class card — and wherever a step takes a connector's from/to, that connector may be an edge, a message, a transition, or a relation. Only ids matter, so a state id or participant id works anywhere a node id does. The examples here happen to be architecture diagrams; the vocabulary carries over unchanged.

Each recipe below is a small live diagram; for the exhaustive list of steps and knobs, see the flow & animation reference.

Let Beck derive a flow

If you write no flow block at all, Beck animates anyway — and what it derives fits the diagram type: architecture and state diagrams get a topological packet-walk from roots to leaves; a sequence plays its messages in authored order. Class diagrams are the exception: they're structural reference material with no sequence of events to play, so they render a still frame unless you script a flow: yourself. Declaring the boxes and connectors is enough to get motion — see derived flow for the exact behaviour per type.

yaml
type: architecture
meta: { title: Auto flow, direction: LR }
nodes:
  - { id: client, title: Client, kind: user }
  - { id: api, title: API, kind: gateway }
  - { id: db, title: Postgres, kind: db }
edges:
  - { from: client, to: api }
  - { from: api, to: db, label: queries }
  
beck
Auto flowqueriesClientAPIPostgres

Reach for an explicit flow when topological order does not match the story you want to tell.

Add a flow block

A flow block has steps — an ordered list where each step is a single-key map — plus repeat and repeatDelay. Set repeat: -1 to loop forever (the default), or repeat: 0 to play once. repeatDelay is the pause in seconds before the loop restarts.

The workhorse step is packet, which sends one dot along an edge. Give it a from, a to, and optionally a label and color.

yaml
type: architecture
meta: { title: Request, direction: LR }
nodes:
  - { id: client, title: Client, kind: user }
  - { id: api, title: API, kind: gateway }
  - { id: db, title: Postgres, kind: db }
edges:
  - { from: client, to: api }
  - { from: api, to: db }
flow:
  repeat: -1
  repeatDelay: 1.5
  steps:
    - packet: { from: client, to: api, label: GET /item }
    - packet: { from: api, to: db, label: SELECT, color: info }
    - packet: { from: db, to: api, color: success }
  
beck
RequestClientAPIPostgresGET /itemSELECT

Note

meta.loop: false forces flow.repeat: 0, so the sequence plays once and stops. meta.animate: false skips the runtime entirely and renders a static frame.

Shape a packet

Every packet inherits motion from its edge's kind — size, speed, glow, and ease — but you can override any of it. Set shape to dot (the default, keeping the edge-kind size), circle, or ring; tune size, speed, glow, ease, and impact (an expanding ring on arrival). The example below sends a slow, decelerating ring that lands with impact.

yaml
type: architecture
meta: { title: Shaped packet, direction: LR }
nodes:
  - { id: ingest, title: Ingest, kind: gateway }
  - { id: store, title: Warehouse, icon: warehouse, kind: db }
edges:
  - { from: ingest, to: store }
flow:
  steps:
    - packet: { from: ingest, to: store, shape: ring, ease: decelerate, size: 14, speed: 220, glow: true, impact: true, color: info }
  
beck
Shaped packetIngestWarehouse

See the flow reference for the full list of knobs and ease names.

Send a burst

A burst fires several packets in waves. Set count for the number of waves and stagger for the gap between them. The clever part: to can be a list, so one source fans out to many targets at once.

yaml
type: architecture
meta: { title: Fan-out, direction: TB }
nodes:
  - { id: broker, title: Broker, kind: queue }
  - { id: a, title: Worker A }
  - { id: b, title: Worker B }
  - { id: c, title: Worker C }
edges:
  - { from: broker, to: a }
  - { from: broker, to: b }
  - { from: broker, to: c }
flow:
  steps:
    - burst: { from: broker, to: [a, b, c], count: 4, stagger: 0.1, color: warn }
  
beck
Fan-outBrokerWorker AWorker BWorker C

Each wave broadcasts a dot to every target in the list, so count: 4 over three workers sends twelve dots in total.

Mark nodes

State changes carry as much story as movement. Several steps annotate a box directly — a node, state, participant, or class card:

  • status sets a persisting pill on the node.
  • working leaves a node breathing (busy) until you clear it with idle or reset.
  • highlight and pulse draw a brief eye to a node.
  • fail shakes the node red and flashes, with optional status text.

Here is a tiny build-then-fail story: the build node goes to work, the tests run, and the deploy fails.

yaml
type: architecture
meta: { title: CI run, direction: LR }
nodes:
  - { id: build, title: Build, icon: code }
  - { id: test, title: Tests, kind: service }
  - { id: deploy, title: Deploy, kind: gateway }
edges:
  - { from: build, to: test }
  - { from: test, to: deploy }
flow:
  steps:
    - working: { node: build }
    - packet: { from: build, to: test, label: artifact }
    - idle: { node: build }
    - status: { node: build, text: built, color: success }
    - pulse: { node: test }
    - packet: { from: test, to: deploy }
    - fail: { node: deploy, text: rollout failed }
  
beck
CI runBuildbuiltTestsDeployrollout failedartifact

Persistent edge effects

Two steps change a connector — an edge, message, transition, or relation — until you reset, which is ideal for showing an established connection rather than a one-off message:

  • activate recolours a path and keeps it lit.
  • stream runs continuous flowing dashes along a path for ongoing traffic.
yaml
type: architecture
meta: { title: Live link, direction: LR }
nodes:
  - { id: producer, title: Producer, kind: service }
  - { id: topic, title: Topic, icon: kafka, kind: queue }
  - { id: consumer, title: Consumer, kind: service }
edges:
  - { from: producer, to: topic }
  - { from: topic, to: consumer }
flow:
  steps:
    - activate: { from: producer, to: topic, color: success }
    - stream: { from: topic, to: consumer, color: info }
    - wait: 2
    - reset:
  
beck
Live linkProducerTopicConsumer

Narrate the story

Motion shows what moves; a caption says why. A narrate step writes a line to a caption bar under the diagram and holds it long enough to read before the flow moves on — a teleprompter that walks the viewer through the animation in words. Drop a narrate between the steps it explains:

yaml
type: architecture
meta: { title: Nightly report, direction: LR }
nodes:
  - { id: cron, title: Scheduler, kind: service, icon: clock }
  - { id: worker, title: Report Worker, kind: service }
  - { id: db, title: Warehouse, kind: db, icon: warehouse }
  - { id: mail, title: Email, kind: external, icon: mail }
edges:
  - { from: cron, to: worker }
  - { from: worker, to: db }
  - { from: worker, to: mail }
flow:
  steps:
    - narrate: At midnight the scheduler wakes the report worker.
    - packet: { from: cron, to: worker, label: run }
    - narrate: The worker pulls last night's numbers from the warehouse.
    - working: { node: db }
    - packet: { from: worker, to: db, color: info }
    - packet: { from: db, to: worker, color: success }
    - idle: { node: db }
    - narrate: { text: "With the figures in hand, it emails the finished report.", color: success }
    - packet: { from: worker, to: mail, label: send }
    - wait: 1.4
beck
Nightly reportSchedulerReport WorkerWarehouseEmailAt midnight the scheduler wakes the report worker.The worker pulls last night's numbers from the warehouse.With the figures in hand, it emails the finished report.runsend

The shorthand narrate: <text> is all you usually need — the caption's hold time is computed from its length, so a longer line lingers longer. When you want more control, use the full form and set hold (seconds, overriding the auto pace) or color (an accent token or CSS colour that tints the line):

yaml
- narrate: The worker pulls last night's numbers from the warehouse.
- narrate: { text: The rollout failed — reverting., hold: 3, color: danger }

Note

Captions are prose, so they often contain commas. The bare narrate: <text> shorthand handles them fine, but inside the braced { } form — or any inline note: — you must quote text with a comma (text: "Approved, and live."), or YAML reads the comma as the end of the value.

You don't have to write a flow at all to get narration. When Beck derives a flow, a note: on the connector becomes the caption for that hop — a note on an architecture edge, a sequence message, or a state transition. It's the quickest way to caption a diagram that already animates itself:

yaml
edges:
  - { from: worker, to: db, note: The worker pulls last night's numbers. }

Tune the reading pace once, for the whole diagram, with meta.narrate — a mapping of wpm (reading speed), min (a floor on each caption's time), and pad (extra lead-in/out). Set meta.narrate: false to drop the caption bar entirely. See the narration reference for the exact knobs and defaults.

Control the sequence

A handful of steps shape timing rather than visuals:

  • parallel runs a list of steps at the same instant.
  • wait pauses for a number of seconds (default 0.5).
  • phase drops a named seek() label you can jump to from script.
  • reset restores the diagram to its initial state.
yaml
type: architecture
meta: { title: Read path, direction: LR }
nodes:
  - { id: client, title: Client, kind: user }
  - { id: api, title: API, kind: gateway }
  - { id: cache, title: Redis, kind: cache }
  - { id: db, title: Postgres, kind: db }
edges:
  - { from: client, to: api }
  - { from: api, to: cache }
  - { from: api, to: db }
flow:
  steps:
    - phase: lookup
    - packet: { from: client, to: api, label: GET /item }
    - parallel: [ { packet: { from: api, to: cache, color: warn } }, { working: { node: db } } ]
    - status: { node: cache, text: miss, color: warn }
    - packet: { from: api, to: db, label: SELECT }
    - idle: { node: db }
    - packet: { from: db, to: api, color: success }
    - wait: 1
    - reset:
  
beck
Read pathClientAPIRedismissPostgresGET /itemSELECT

Respect reduced motion

When a visitor has prefers-reduced-motion set, or you author meta.animate: false, Beck renders the static frame instead. Write your flow for the animated case — the static fallback is automatic, so you never script two versions.

Next steps