quarto-revealjs-magic-move
Animated Magic Move transitions for Quarto revealjs presentations
Code, math, and SVG output morph smoothly between states instead of hard-cutting, so viewers can visually track what changed. Built on Quarto’s native syntax highlighting, with no external JavaScript dependencies.
Installation
quarto add emilhvitfeldt/quarto-revealjs-magic-moveThen add the plugin to your document’s YAML header:
---
format: revealjs
revealjs-plugins:
- magic-move
---Usage
There are two ways to mark up a magic-move sequence: div-based (fragments within a single slide) and slide-based (consecutive slides).
Div-based (fragments)
Wrap consecutive code blocks in a magic-move div. Each block becomes a step, and pressing space/arrow keys steps through the animation as fragments on one slide.
::: magic-move
```javascript
if (condition) a else b
```
```javascript
if (condition) {
a
} else {
b
}
```
:::Slide-based
Add the .magic-move class to consecutive slide headers instead. Each slide is a full step, and the animation plays as you navigate from one slide to the next.
## Step 1 {.magic-move}
```r
x <- 1
```
## Step 2 {.magic-move}
```r
x <- 1
y <- 2
```A slide without .magic-move breaks the sequence, so you can freely mix animated and regular slides.
What can animate
Magic-move works on:
- Code blocks (any language Quarto highlights) — tokens are matched and moved between states
- Math (
$$ ... $$) — equations morph between steps - SVG output — consecutive
.magic-moveslides that each render an inline SVG have their shapes (rect,circle,line,polyline,polygon,ellipse,path, and R-style glyph text) matched and morphed between states. This works with any plotting library whose SVG output uses plain, literal shape elements with stable attributes across renders — verified with:- R: ggplot2, grid, base R graphics, lattice (all render through R’s
dev: svgCairo device) - Python: matplotlib, seaborn, plotnine (bar/line/area marks — not scatter/point markers, see limitations), and Altair with
alt.renderers.enable("svg") - Julia: CairoMakie, and Plots.jl with the GR backend (
Plots.gr(fmt = :svg))
- R: ggplot2, grid, base R graphics, lattice (all render through R’s
Limitations
- Token matching is content-based; significantly different code may fade in/out instead of animating smoothly
- Coarse tokenization from Quarto’s syntax highlighter means very fine-grained punctuation animation isn’t always perfect
- SVG shape matching has no fallback for elements whose grouping key (fill/stroke/clip-path) differs between renders. This breaks a few specific cases:
- Plotly (via kaleido) and ggiraph’s
girafe()widgets stamp a random clip-path id on every render, so shapes never match across slides and the transition falls back to a hard cut - Gadfly.jl positions marks via an ancestor
<g transform>rather than each element’s own coordinates, which the matcher doesn’t read - matplotlib/seaborn/plotnine scatter or point markers render as
<use>glyph references rather than literal<circle>/<path>shapes, so they don’t animate (bars/lines/areas from the same libraries are unaffected) - Charts rendered via Quarto’s Observable JS (
{ojs}) engine — e.g. D3, Observable Plot, Vega-Lite — produce fully compatible SVG shapes, but render after Reveal’sreadyevent, while this extension only scans for SVG content once, atready. So{ojs}-rendered charts never get picked up as a magic-move sequence at all, regardless of their SVG shape compatibility.
- Plotly (via kaleido) and ggiraph’s
Examples
Code — div-based fragments
Code — slide-based
SVG output — ggplot2 / grid
SVG output — other R packages
SVG output — Python
SVG output — Julia
Charts rendered via Quarto’s Observable JS ({ojs}) engine — D3, Observable Plot, Vega-Lite — don’t animate yet (see Limitations above); no example is shown here for that reason.
License
MIT