Features
Adding New Elements
Click the Add button to open a submenu with options:
- Text: Creates a new editable text block on the current slide. Position and style it like any other editable element.
- Slide: Inserts a new blank slide after the current one. The slide will be saved with a
## New Slideheading. - Arrow: Adds an arrow to the current slide (requires quarto-arrows extension). See Arrow Support below.
New elements are marked with a dashed border to distinguish them from original content.
Modify Mode
Not every element needs to be marked with {.editable} in advance. Click the Modify button in the menu bar to enter modify mode, then click any highlighted element to make it editable on the fly.
Elements that can be made editable are highlighted with a green ring as soon as you enter modify mode. Click a highlighted element to activate it. Modify mode exits automatically after activation (except for slide titles, which stay in edit mode until you click away or press Enter). Press Escape at any time to exit modify mode without activating anything; focus returns to the Modify button.
After saving, changes are written back to your .qmd file.
Currently supported element types:
| Element | Source syntax | Operations |
|---|---|---|
| Plain paragraph | Some paragraph text |
Move, resize, rotate, text editing |
| Plain image |  |
Move, resize, rotate |
| Inline image | text  text |
Move, resize, rotate |
{.absolute} image |
{.absolute left=Xpx top=Ypx ...} |
Move, resize, rotate |
| Plain video |  |
Move, resize, rotate |
{.absolute} div |
::: {.absolute left=Xpx top=Ypx ...} |
Move, resize, rotate, text editing |
| Classed fenced div | ::: {.my-class} |
Move, resize, rotate, text editing |
| Id-keyed fenced div | ::: {#my-id} |
Move, resize, rotate, text editing |
| Callout block | ::: {.callout-note} (and other callout types) |
Move, resize, rotate |
| Column layout | ::: {.columns} |
Move, resize, rotate |
| Table (pipe, grid, HTML, list) | \| A \| B \| / +---+---+ / <table>…</table> / ::: {.list-table} |
Move |
| Display code block | ```python / ``` (non-executed) |
Move, resize |
| Code chunk output | ```{r}, ```{python}, ```{ojs} producing non-image output |
Move, resize |
| Code chunk figure | ```{r} / ```{python} chunk producing exactly one <img> figure |
Move, resize, rotate |
| Diagram chunk | ```{mermaid}, ```{dot} (Graphviz) |
Move, resize |
| Display equation | $$E = mc^2$$ (single- or multi-line) |
Move |
| Slide title | ## Heading text |
Text editing with formatting toolbar |
| Positioned arrow | [arrow: missing coordinates] |
Move, resize (full styling preserved) |
Plain paragraphs are matched by their position among top-level paragraphs on the slide (Nth paragraph in the DOM = Nth paragraph block in source). On save, the paragraph is wrapped in a ::: {.absolute ...} fenced div. If the text was edited with the Quill editor, the new content is written back; otherwise the original QMD source text is preserved as-is.
Plain images and videos are inserted directly in a slide without positional attributes. Inline images embedded mid-paragraph (text  text) work the same way as plain images — the same ](src) write-back regex matches both. Activating an inline image pulls it out of the text flow, so the surrounding paragraph text reflows around the now-absolutely-positioned image. Activation is a one-way transform: once saved with .absolute attrs, the image is no longer inline on re-render — Quarto positions it out of flow and the paragraph reads as one continuous sentence. Going back to the inline form requires editing the source .qmd by hand. {.absolute} images and divs carry left=, top=, width=, and height= attributes and are matched back to the source using those values.
Fenced divs (classed, id-keyed, callouts, and column layouts) are matched by their class or id within each slide’s source chunk, with positional indexing as a fallback for keyless divs. On save, the opening fence line gains .absolute positional attrs. Callout blocks are wrapped in an outer :::: {.absolute ...} div instead, since Quarto’s callout renderer does not forward positional attrs from the callout fence itself.
Tables in pipe, grid, raw-HTML, and list-table syntax are matched to their source by the first (header) row of the table within the slide chunk; if the same header row appears more than once, matching falls back to position. A trailing Pandoc caption line (: caption {#tbl-id} or Table: caption) is included in the wrapped block so the caption stays anchored to the table. On save, the entire table block is wrapped in ::: {.absolute left=Xpx top=Ypx}\n…\n::: (for list tables, the outer .absolute fence wraps the existing ::: {.list-table} block). Resize and rotate are intentionally not provided — table width is determined by content and column definitions, not a CSS dimension on a wrapper. Computational tables produced by executable chunks are handled by the code-chunk-output path.
Non-executed display code blocks (```python, ``` with no language, etc.) are matched to their source by position among the top-level code fences on the slide, anchored to the first non-empty line of code content. On save, the entire fenced code block is wrapped in ::: {.absolute left=Xpx top=Ypx width=Wpx height=Hpx}\n…\n:::. Text editing is not provided — the source remains literal code.
Executable code chunks that render non-image output — HTML tables, printed stdout, interactive widgets, and Observable JS (```{ojs}) — are activatable as a single block (the <div class="cell"> wrapping the chunk’s code and output). Named chunks are matched to source by #| label: or fence-token label; unnamed chunks are matched positionally. On save, the entire chunk (fence to fence) is wrapped in ::: {.absolute left=Xpx top=Ypx width=Wpx height=Hpx}\n…\n:::. Chunks producing only images aren’t activatable through this path — the figure itself is the meaningful target.
Diagram chunks (```{mermaid} and ```{dot}) follow the same path. Their SVG output is rendered into the same <div class="cell"> wrapper, so they are matched, activated, and written back identically to other executable chunks — named by #| label: or fence-token label, otherwise positionally.
Display equations ($$...$$, both single- and multi-line) are matched to source by the LaTeX body line, with positional fallback when the same line appears more than once. Equations inside fenced code blocks or other ::: fenced divs are skipped. On save, the entire $$...$$ block is wrapped in ::: {.absolute left=Xpx top=Ypx}\n…\n:::. Resize, rotate, and text editing are intentionally not provided — equation size is set by the LaTeX content and the content itself is raw LaTeX, not prose.
Slide titles open an inline formatting toolbar (bold, italic, underline, strikethrough, text color, background color) when clicked. The title can be re-activated to edit again after clicking away.
Re-activating saved positions. Once an element has been positioned and saved (wrapped in ::: {.absolute …}), it is re-activatable on the next render — modify mode targets the inner semantic element (the <p>, <table>, <ul>, …) and keeps its type-specific capabilities. Paragraphs continue to offer Quill text editing; tables and equations stay move-only; lists, blockquotes, display code, code chunks, and figures keep their first-activation capabilities. On save, the existing {.absolute …} block is rewritten in place — no nested wrappers. Wrappers with content that doesn’t match a typed inner (e.g. multi-child or raw HTML) continue to re-activate as generic Positioned divs.
Positioned arrows from previous saves ([arrow: missing coordinates] shortcodes rendered via quarto-arrows) are activatable too. Click a highlighted arrow to drag and resize it via the standard arrow handles; the source’s color, width, head, dash, line, opacity, label, curve, and waypoints are preserved. On save, the matching shortcode is rewritten in-place with the new coordinates. Arrows whose shortcodes use kwargs the editable system doesn’t yet round-trip (bend, fragment, aria-label, class, head-fill, …) get an amber warning ring instead, so their source attributes aren’t silently dropped.
Single-figure code chunks (an {r} or {python} chunk producing exactly one <img>) are activatable as described above. Multi-figure chunks (2+ image outputs) show an amber warning ring and are not yet activatable. Non-image chunk outputs — tables, printed text, widgets, OJS — are supported as described above.
Rich Text Editing
Click the edit button on any editable text element to open the Quill rich text editor with a formatting toolbar:
| Button | Format | Saved As |
|---|---|---|
| B | Bold | **text** |
| I | Italic | *text* |
| U | Underline | [text]{.underline} |
| S | Strikethrough | ~~text~~ |
| Color | Text Color | [text]{style='color: ...'} |
| Background | Background Color | [text]{style='background-color: ...'} |
| Alignment | Left/Center/Right | Fenced div with style="text-align: ..." |
Click the edit button again to exit edit mode.
Note: In this demo, the Save button will download a file. With quarto preview, it saves directly to your .qmd file.
Color Picker
The color picker provides three ways to select colors:
- Unset - Remove any color formatting (returns to default)
- Preset Colors - A palette of 18 commonly used colors
- Custom… - Opens the system color picker for any color
The preset palette appears when you click the color or background color button in the menu bar.
Brand Color Support
If your project uses Quarto’s brand system with a _brand.yml file, the color picker will automatically use your brand palette instead of the default colors.
Setup
Create a _brand.yml file in your project directory:
color:
palette:
primary: "#FF6B6B"
secondary: "#4ECDC4"
accent: "#2C3E50"Shortcode Output
When you use a brand color, it saves using Quarto’s brand shortcode syntax:
[text]{style='color: {{< brand color primary >}}'}This ensures your colors stay synchronized with your brand definition. Non-brand colors (from the custom picker) save as raw values.
Undo/Redo
Made a mistake? Use keyboard shortcuts to undo and redo your changes:
- Ctrl+Z (or Cmd+Z on Mac) to undo the last action
- Ctrl+Y or Ctrl+Shift+Z (or Cmd+Shift+Z on Mac) to redo
Undo/redo tracks:
- Element position changes (drag)
- Element size changes (resize)
- Element rotation changes
- Font size changes
- Text alignment changes
Text content editing (in edit mode) uses the browser’s native undo, which is separate from the extension’s undo stack.
Image Sizing Tip
Sometimes you might find that images don’t stay the size that you dragged them to be. This is because the default is to set max-width and max-height to 95%. You can fix this by adding the following to your SCSS file:
.reveal img {
max-width: unset;
max-height: unset;
}Image Controls
When you click an image element, the menu bar switches to an image-specific context panel with controls for styling the selected image.
Opacity
A slider (0–100%) sets the image’s CSS opacity. Changes apply immediately and are saved to the QMD style= attribute on the next Save.
Border Radius
A number input (pixels) rounds the corners of the image with border-radius. Set to 0 for sharp corners.
Crop
Click the ✂ button to enter crop mode. While active, the corner resize handles adjust the image’s clip-path inset instead of resizing the element — dragging a corner inward crops that edge. The handles move with the crop boundary so you can see exactly what will be visible. Click the button again to exit crop mode.
Crop values are saved as clip-path: inset(top right bottom left) in the QMD style= attribute.
Flip
Two toggle buttons flip the image:
- ⇆ — flips horizontally (
scaleX(-1)) - ⇅ — flips vertically (
scaleY(-1))
Flips compose correctly with any existing rotation into a single transform: declaration.
Replace Image
Opens a file picker to swap the image source. The element’s height is automatically recalculated to match the new image’s aspect ratio while keeping the current width. The filename is written back to the QMD on Save.
Place the replacement image file in the same folder as your QMD file before rendering. A reminder popup appears after selecting a file.
Reset
Reverts opacity, border radius, crop, and flips to their defaults without affecting the element’s position or size.
Arrow Support
The extension integrates with quarto-arrows to add arrows to your slides visually.
Setup
First, install the quarto-arrows extension:
quarto add EmilHvitfeldt/quarto-arrowsThen add both filters to your YAML header:
revealjs-plugins:
- editable
filters:
- editable
- arrowsIf you try to add an arrow without the quarto-arrows extension installed, you’ll see a warning with install instructions. You can still add arrows in the browser—they just won’t render when you next run quarto render until the extension is installed.
Using Arrows
Click Add → Arrow to create an arrow on the current slide. The arrow appears centered with draggable handles:
- Blue handle: Start point
- Green handle: End point
- Drag body: Click and drag anywhere on the arrow line to move the entire arrow
Curved Arrows
Toggle Curve in the menu bar (when arrow is selected) to enable Bezier curve mode. Two additional control point handles appear:
- Orange handle: First control point (influences curve near start)
- Purple handle: Second control point (influences curve near end)
Dashed guide lines show the connection between endpoints and their control points.
Selection
Only the currently selected arrow shows its handles and controls. Click on an arrow to select it, or click elsewhere to deselect.
Arrow Styling
When an arrow is selected, the menu bar switches to show styling controls:
- Curve: Toggle between straight and curved arrows
- Color: Color presets (including brand colors) plus custom color picker
- Width: Line thickness (1-20px)
- Head: Arrowhead style (arrow, stealth, diamond, circle, square, bar, or none)
- Dash: Line pattern (solid, dashed, or dotted)
- Line: Line style (single, double, or triple parallel lines)
- Opacity: Transparency slider (0 to 1)
Label options:
- Label text: Add a text label to the arrow
- Label position: Choose where the label appears (start, middle, or end of the arrow)
- Label offset: Distance from the arrow line (positive values place label above/left, negative below/right)
Labels automatically rotate to follow the arrow direction and update color to match the arrow.
Click anywhere outside the arrow to deselect it and return to the normal menu bar. Style changes are applied immediately and saved with the arrow.
Saved Format
Arrows are saved as quarto-arrows shortcodes:
{{< arrow from="100,200" to="400,300" position="absolute" >}}Curved arrows include control points:
{{< arrow from="100,200" to="400,300" control1="150,100" control2="350,400" position="absolute" >}}Styled arrows include additional attributes:
{{< arrow from="100,200" to="400,300" color="#ff0000" width="4" head="stealth" dash="dashed" opacity="0.8" position="absolute" >}}Arrows with labels:
{{< arrow from="100,200" to="400,300" label="Step 1" label-position="middle" label-offset="10" position="absolute" >}}