Timer — auto-playing animation
Use
timerto declare a value variable that auto-advances — steps from min to max at a fixed cadence, then either loops back to the start or stops. The "time dimension" sibling of Interact controls — slider is manual drag, timer is auto-step. Pair with Vega chart / SVG scenes for animation.
When to use
- ✅ Demo animations (sine wave flowing / spring oscillating / dot orbiting)
- ✅ Data replay (frame-by-frame walkthrough of a time series)
- ✅ Algorithm auto step-through (play, then stop it at the moment you want to inspect)
- ✅ Countdown / counter / pomodoro
- ❌ Very long durations (small
stepor hugemax-min→ browser render strain)
Basic syntax
```interact
timer t range=0..10 step=0.1 speed=50 loop=true label="Time"
template stl:
[Current time] -> [{round(t, 1)}s]
```
timer <var> range=A..B step=S speed=N loop=true|false [label="..."] declares a timer variable.
| Attribute | Meaning |
|---|---|
range=A..B |
Start value A, end value B |
step=S |
Per-tick increment |
speed=N |
Tick interval in milliseconds — smaller N ticks faster. Default 50 (~20 ticks/second); the reader clamps it to a floor of 16ms so an author can't accidentally spin the CPU |
loop=true|false |
true (default): on reaching B, jump back to A and keep going. false: run to B a single time, then stop |
label="..." |
Display label |
There are only these four attributes. There is no separate "mode" parameter beyond loop=true|false — no third state and no reverse-sweep keyword exist in the grammar. Every timer renders the same built-in widget regardless of loop: a progress bar, a value readout, a ▶ Play / ⏸ Pause button, and a 🔁 Reset button. A timer always starts in a stopped state — the reader clicks Play to start it. loop=false is what makes it run through a single pass and then stop; Reset always jumps back to default (or A if default isn't set).
Renders: stopped timer widget. Reader clicks Play → steps from 0 to 10 (+0.1 every 50ms) → loops back to 0 and keeps going, until they click Pause.
Examples
Example 1: flowing sine wave
```interact
timer t range=0..6.28 step=0.05 speed=50 loop=true label="Time"
template vega-lite:
{
"width": 500,
"height": 200,
"data": {"sequence": {"start": 0, "stop": 6.28, "step": 0.05, "as": "x"}},
"transform": [
{"calculate": "sin(datum.x + {t})", "as": "y"}
],
"mark": "line",
"encoding": {
"x": {"field": "x", "type": "quantitative"},
"y": {"field": "y", "type": "quantitative", "scale": {"domain": [-1.5, 1.5]}}
}
}
```
Renders: sine wave flows continuously right to left, like an oscilloscope, after Play is clicked. t increments forever (loop=true); sin(x + t) shifts the whole curve's phase.
Example 2: spring oscillator (position drawn live)
```interact
timer t range=0..10 step=0.05 speed=50 loop=true label="Time"
template svg:
<svg viewBox="0 0 200 100">
<line x1="20" y1="50" x2="180" y2="50" stroke="#ddd" stroke-dasharray="2"/>
<circle cx="{round(100 + 60*sin(2*pi*t/2), 1)}" cy="50" r="6" fill="red"/>
</svg>
```
Renders: red dot does simple harmonic motion along a horizontal line (left-right back-and-forth), period 2 seconds, after Play is clicked. SVG cx is driven live by t.
Example 3: single-pass playback
```interact
timer t range=0..60 step=0.1 speed=50 loop=false label="Elapsed"
template vega-lite:
{
"data": {"sequence": {"start": 0, "stop": {t}, "step": 0.5, "as": "x"}},
"transform": [{"calculate": "{t}", "as": "y"}],
"mark": "line",
"encoding": {
"x": {"field": "x"},
"y": {"field": "y", "type": "quantitative"}
}
}
```
Renders: click Play → runs 0 to 60 through a single pass and stops at the end (loop=false). Click Reset to jump back to 0 and Play again.
Example 4: ping-pong via mod + abs (no native mode — build it from a computed)
This behavior isn't a built-in keyword — it's assembled from a loop=true timer plus a computed fold. To get a back-and-forth sweep, run the timer over twice the range you want to visit, then fold it back with abs/mod:
```interact
timer t range=0..720 step=2 speed=50 loop=true label="Drive"
computed angle = 360 - abs(mod(t, 720) - 360)
template svg:
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="none" stroke="#ddd"/>
<line x1="50" y1="50"
x2="{round(50 + 40*cos(angle*pi/180), 2)}"
y2="{round(50 + 40*sin(angle*pi/180), 2)}"
stroke="red" stroke-width="2"/>
</svg>
```
Renders: red line rotates from 0° to 360° (clockwise), then reverses back to 0°, then forward again — a smoother back-and-forth than a raw loop jump-back. angle = 360 - abs(mod(t, 720) - 360) folds a 0..720 driver into a 0..360..0 triangle wave; that's an ordinary computed line, not a timer feature.
Plain-text fallback behavior
```interact
timer t range=0..10 step=0.1 speed=50 loop=true label="Time"
template stl:
[Current time] -> [{round(t, 1)}s]
```
→ Non-supporting readers show as a code block — readers see the timer t range=0..10 step=0.1 speed=50 loop=true declaration + template, understand the original meant to animate.
| Reader | Render |
|---|---|
| Rho MD | Full animation + Play/Pause/Reset controls |
| GitHub web | Code block (timer declaration visible) |
| Obsidian | Same |
| VS Code default preview | Same |
| cat / less | Plain text |
Common pitfalls
1. step too small → browser hangs
timer t range=0..100 step=0.0001 speed=50 loop=true ← ❌ 1,000,000 ticks per cycle; browser dies
timer t range=0..100 step=0.5 speed=50 loop=true ← ✅ 200 ticks; ~10s per cycle at 50ms/tick
Rule of thumb: (max - min) / step ≈ 100-1000 ticks for the most comfortable feel.
2. Multiple timers — sync issues
```interact
timer t1 range=0..10 step=0.1 speed=50 loop=true label="t1"
timer t2 range=0..10 step=0.1 speed=50 loop=true label="t2"
```
→ t1 and t2 are independent — each has its own Play/Pause state, and if a reader starts them at different moments, phases drift apart. For strict sync, use one timer + computed derivations:
timer t range=0..10 step=0.1 speed=50 loop=true label="Time"
computed t1 = t
computed t2 = mod(t + 5, 10) ← Phase-shifted by 5s
3. Expecting a discrete "step forward" button
There is no discrete click-to-advance control type in the spec, and chip/select set a value on selection rather than incrementing one. For "algorithm auto step-through," play the timer and stop it manually at the moment you want to inspect (Play/Pause is built into every timer widget); there's no separate one-tick-per-click primitive to reach for instead.
4. loop= misspelled or wrong type
timer t range=0..10 step=0.1 speed=50 loop=yes ← ❌ not a boolean literal
timer t range=0..10 step=0.1 speed=50 Loop=true ← ❌ attribute names are case-sensitive
timer t range=0..10 step=0.1 speed=50 loop=true ← ✅
timer t range=0..10 step=0.1 speed=50 loop=false ← ✅ runs through a single pass, then stops
Only true / false are valid — there's no third "mode" value.
5. Timer name clashes with slider name
```interact
slider t range=0..10 step=1 default=5 label="t"
timer t range=0..10 step=0.1 loop=true label="t" ← ❌ Duplicate declaration
```
→ Names must be unique. Timer names can't collide with slider/input/computed.
6. Expecting loop=false to auto-reset
timer t range=0..10 step=0.1 speed=50 loop=false label="t"
→ loop=false runs to max and stops; the widget's own 🔁 Reset button (always present) is how a reader plays it again — there's no scripting hook to auto-replay from the document side.
7. Timer paired with chart but transform.calculate doesn't use the timer
```interact
timer t range=0..10 step=0.1 speed=50 loop=true label="Time"
template vega-lite:
{
...
"transform": [{"calculate": "sin(datum.x)", "as": "y"}] ← ❌ Doesn't reference {t}
}
```
→ The curve never moves because calculate doesn't depend on the timer. Reference {t} in the vega expression: "sin(datum.x + {t})".
See also
- Interact controls — Manual controls (slider / input / select / toggle)
- Interact + Vega-Lite chart — Chart paired with timer for animation
- SVG scenes — SVG paired with timer for physics simulations
- Computed — Derived values (multi-timer sync, ping-pong folding)
- Shared state (namespace)
- Plain-text fallback principle