Home

Figures

In Markdown, an image on its own line renders as a block-level image — but it has no caption, no label, and no way to refer to it from elsewhere in your document. A figure wraps one or more images (or other content) and adds:

  • Labels: automatically numbered identifiers like "Figure 1", "Figure 2", etc.

  • Captions: descriptive text rendered below the figure.

  • Multi-panel layouts: arrange subfigures in grids with a concise layout mini-language.

  • Overlays: transparent SVG annotation layers for arrows, callouts, bounding boxes, and labels.

Use a plain block image when you just want to show a picture. Use a figure when the image is a first-class part of your document that readers need to identify and reference.

Syntax

A figure in Stencila Markdown starts with a colon fence line with the keyword figure and optional label, layout, and padding. In a basic figure there is usually a single image and a caption paragraph.


::: figure <label> #<id> [<layout>] {pad="<padding>"}

![](image.png)

The figure caption.

:::

Labels

Every figure automatically receives a sequential label — "Figure 1", "Figure 2", and so on — based on its position in the document. You do not need to manage these numbers yourself; if you reorder figures, the labels update to match.

You can set an explicit label by placing it after the figure keyword. However, it is almost always better to omit explicit labels and let Stencila assign them automatically. Explicit labels do not update when you rearrange your document and can easily get out of sync.

IDs and cross-referencing

Every figure automatically receives an id derived from its label with a fig- prefix — a figure labelled "1" gets fig-1, a subfigure labelled "1A" gets fig-1a. You can link to any figure using a standard Markdown link with a # target:

See [Figure 1](#fig-1) for an overview.

Subfigure [Figure 4a](#fig-4a) shows the red panel, while [Figure 4c](#fig-4c) shows the blue panel.

See Figure 1 for an overview.

Subfigure Figure 4a shows the red panel, while Figure 4c shows the blue panel.

Auto-derived IDs like fig-1 change whenever you add, remove, or reorder figures. For figures that are referenced from other documents, external links, or APIs, you can assign a stable ID with #<id> on the fence line:


::: figure #specimen-1

![](specimen-1.png)

Photograph of the first specimen.

:::

This figure is always reachable at #specimen-1 regardless of its position in the document.

Captions

Inside a ::: figure fence, Stencila separates content from caption automatically:

  • A paragraph containing only an image (or audio, or video) becomes content.

  • Any other paragraph becomes caption.

The caption can appear before or after the image — Stencila treats it the same way. Both of these produce identical output:


::: figure

A blue placeholder image.

![](images/600x300-blue.png)

:::

Figure 1: A blue placeholder image.

You can include multiple paragraphs in the caption:


::: figure

![](images/600x300-teal.png)

This is the first paragraph of the caption.

This is a second paragraph providing additional detail about the figure.

:::

Figure 2: This is the first paragraph of the caption.

This is a second paragraph providing additional detail about the figure.

Executable figures

Figures are not limited to images loaded from local files or remote URLs. They can also contain visual output generated by executable code blocks, which makes them useful for reproducible charts, plots, and other computed graphics.

This is especially helpful when the figure should stay in sync with the data or analysis in your document. The executable code generates the figure content, and the surrounding figure still provides the usual captioning, numbering, and cross-referencing. The example below uses Plotly JSON because it is lightweight and built in to Stencila, but the same pattern can be used with figures generated from Python, R, or other supported execution kernels.


::: figure

```plotly exec
{
  "data": [
    {
      "type": "bar",
      "x": ["A", "B", "C", "D", "E"],
      "y": [4, 7, 2, 9, 5]
    }
  ],
  "layout": {
    "xaxis": {"title": {"text": "Category"}},
    "yaxis": {"title": {"text": "Value"}}
  }
}
```

A plot of value by category.

:::

Figure 3: A plot of value by category.

Subfigures

A figure can contain nested figures as subfigures. Each subfigure gets its own sub-label (A, B, C, …) and optional caption, while the parent figure holds the overall caption.


::: figure

    ::: figure

    ![](images/400x250-purple.png)

    A purple panel.

    :::

    ::: figure

    ![](images/400x250-blue.png)

    A blue panel.

    :::

A figure with two subfigures stacked vertically.

:::

Figure 4: A figure with two subfigures stacked vertically. (A) A purple panel. (B) A blue panel.

Grid layouts

To arrange subfigures in a grid, add a layout specification in square brackets after figure:


::: figure [<layout>]

:::

The layout mini-language is designed to make simple layouts easy to write while keeping more complex arrangements possible. Rather than requiring full CSS, it provides a compact shorthand that covers the most common multi-panel figure patterns.

IntentLayoutDescription
Single column(none)Default — subfigures stack vertically in a single column
Single row[row]All subfigures side-by-side in a single row
Two equal columns[2]Subfigures flow into a 2-column grid
Three equal columns[3]Subfigures flow into a 3-column grid
Unequal columns[30 70]Proportional widths (30:70 split)
Columns with gap[40 g20 40]Two columns with a gap between them
Layout map[a b | a c]Explicit placement — a spans two rows
Map + widths[30 70 : a b | a c]Widths and placement combined
Empty cell[a . | b c]. leaves a cell empty

Single row

The keyword row places all subfigures side-by-side in a single row without having to count them:


::: figure [row]

    ::: figure

    ![](images/300x200-purple.png)

    Purple.

    :::

    ::: figure

    ![](images/300x200-teal.png)

    Teal.

    :::

    ::: figure

    ![](images/300x200-blue.png)

    Blue.

    :::

Three panels in a single row.

:::

Figure 5: Three panels in a single row. (A) Purple. (B) Teal. (C) Blue.

Column count

A single integer specifies the number of equal-width columns. Subfigures flow left-to-right, top-to-bottom:


::: figure [2]

    ::: figure

    ![](images/400x250-purple.png)

    Purple.

    :::

    ::: figure

    ![](images/400x250-teal.png)

    Teal.

    :::

    ::: figure

    ![](images/400x250-blue.png)

    Blue.

    :::

    ::: figure

    ![](images/400x250-orange.png)

    Orange.

    :::

Four panels in a two-column grid.

:::

Figure 6: Four panels in a two-column grid. (A) Purple. (B) Teal. (C) Blue. (D) Orange.

Column widths

Space-separated integers define proportional column widths. The numbers are relative — 30 70 means a 30:70 split:


::: figure [30 70]

    ::: figure

    ![](images/300x300-purple.png)

    Narrow left panel.

    :::

    ::: figure

    ![](images/700x300-teal.png)

    Wide right panel.

    :::

Unequal two-column layout with a 30:70 width ratio.

:::

Figure 7: Unequal two-column layout with a 30:70 width ratio. (A) Narrow left panel. (B) Wide right panel.

Column gaps

Add a gap between columns using the gN token. For example, 40 g20 40 means two columns of equal width with a gap between them:


::: figure [40 g20 40]

    ::: figure

    ![](images/400x250-orange.png)

    Left.

    :::

    ::: figure

    ![](images/400x250-blue.png)

    Right.

    :::

Two columns with an explicit gap.

:::

Figure 8: Two columns with an explicit gap. (A) Left. (B) Right.

Layout maps

For precise control over where each subfigure appears, use a layout map. Letters represent subfigures (a = first subfigure, b = second, etc.), spaces separate columns, and pipes (|) separate rows:

LayoutMeaning
a bTwo subfigures side-by-side
a b | a ca spans two rows on the left; b and c stack on the right
a a | b ca spans two columns on top; b and c sit below
a . | b cTop-right cell is empty

Spanning rows

Repeating a letter across rows makes that subfigure span vertically:


::: figure [a b | a c]

    ::: figure

    ![](images/400x500-purple.png)

    Spans both rows.

    :::

    ::: figure

    ![](images/400x250-teal.png)

    Top right.

    :::

    ::: figure

    ![](images/400x250-blue.png)

    Bottom right.

    :::

One tall panel on the left with two stacked panels on the right.

:::

Figure 9: One tall panel on the left with two stacked panels on the right. (A) Spans both rows. (B) Top right. (C) Bottom right.

Spanning columns

Repeating a letter across columns makes that subfigure span horizontally:


::: figure [a a | b c]

    ::: figure

    ![](images/800x250-orange.png)

    Spans both columns.

    :::

    ::: figure

    ![](images/400x250-purple.png)

    Bottom left.

    :::

    ::: figure

    ![](images/400x250-teal.png)

    Bottom right.

    :::

A wide panel on top with two panels below.

:::

Figure 10: A wide panel on top with two panels below. (A) Spans both columns. (B) Bottom left. (C) Bottom right.

Empty cells

Use . to leave a cell empty:


::: figure [a . | b c]

    ::: figure

    ![](images/400x250-purple.png)

    Top left.

    :::

    ::: figure

    ![](images/400x250-teal.png)

    Bottom left.

    :::

    ::: figure

    ![](images/400x250-blue.png)

    Bottom right.

    :::

Top-right cell left intentionally empty.

:::

Figure 11: Top-right cell left intentionally empty. (A) Top left. (B) Bottom left. (C) Bottom right.

Combining column widths with layout maps

For layouts that need both unequal column widths and precise placement, put the column specification before a : and the layout map after it:


::: figure [30 70 : a b | a c]

    ::: figure

    ![](images/300x500-purple.png)

    Narrow left, spans both rows.

    :::

    ::: figure

    ![](images/700x250-teal.png)

    Wide right, top.

    :::

    ::: figure

    ![](images/700x250-blue.png)

    Wide right, bottom.

    :::

A 30:70 split with the left panel spanning both rows.

:::

Figure 12: A 30:70 split with the left panel spanning both rows. (A) Narrow left, spans both rows. (B) Wide right, top. (C) Wide right, bottom.

Mixed image sizes

Subfigure images do not need to share the same aspect ratio. When images in the same row have different heights, each image preserves its natural proportions and is vertically centered within the row:


::: figure [2]

    ::: figure

    ![](images/300x150-purple.png)

    Wide (2 : 1).

    :::

    ::: figure

    ![](images/250x200-teal.png)

    Tall (5 : 4).

    :::

    ::: figure

    ![](images/400x250-blue.png)

    Medium (8 : 5).

    :::

    ::: figure

    ![](images/200x100-orange.png)

    Wide (2 : 1).

    :::

Subfigures with different aspect ratios — each image keeps its proportions and centers vertically.

:::

Figure 13: Subfigures with different aspect ratios — each image keeps its proportions and centers vertically. (A) Wide (2 : 1). (B) Tall (5 : 4). (C) Medium (8 : 5). (D) Wide (2 : 1).

Overlays

An overlay is a transparent SVG layer rendered on top of a figure's visual content. Use overlays to add annotations — circles, arrows, bounding boxes, text labels — without modifying the underlying image.

Because each overlay is attached to a specific figure or subfigure and uses SVG viewBox scaling, annotations stay locked to the content they describe regardless of how the page is displayed. On a wide desktop screen a two-column layout renders side by side with each panel's overlay covering exactly that panel; on a narrow mobile screen the same grid collapses to a vertical stack and the overlays scale down with their panels, keeping every circle, arrow, and label in the right place. This means you author annotations once and they work everywhere — web, mobile, print, and PDF — without any manual adjustment.

Inside a ::: figure fence, add a fenced code block with the info string svg overlay containing an <svg> element. The SVG viewBox defines the coordinate system used for your annotations. Matching the dimensions of the underlying content is often the simplest option because coordinates then map directly to image or chart pixels, but it is not required: you can use any viewBox that is convenient, such as 0 0 1000 1000, as long as you author the annotation geometry in that same coordinate space.

Overlay shapes and text inherit theme defaults — stroke color, stroke width, fill, font family, font size, and font weight — from the --figure-overlay-* design tokens. In common cases you only need to specify geometry; when you need a specific color, weight, dash pattern, or other non-default treatment on an element, set it explicitly as an inline SVG attribute and it will take precedence over the theme default.

The examples below use overlay components — high-level elements like <s:callout>, <s:arrow>, <s:scale-bar>, and <s:roi-rect> that expand to standard SVG during compilation. Components handle arrowhead definitions, label positioning, and coordinate math automatically. You can also write raw SVG directly; see the overlay components reference for the full component catalog and details on mixing components with raw SVG.

Simple figure overlay

A single annotation overlay on a figure. Here the viewBox="0 0 600 300" matches the 600×300 placeholder image, so coordinates map directly to pixel positions. This direct mapping is convenient for static images, but it is just a convention rather than a requirement — you can use any viewBox that suits your needs:


::: figure

![](images/600x300-blue.png)

```svg overlay
<svg viewBox="0 0 600 300" xmlns:s="https://stencila.io/svg">
  <s:halo cx="300" cy="150" r="20" width="10"/>
  <s:callout x="370" y="145" label="Center" to-x="300" to-y="150"/>
  <s:roi-rect x="30" y="30" width="160" height="80" label="Region A" label-position="below" stroke-style="dashed"/>
  <s:arrow x="450" y="50" to-x="550" to-y="250" label="Trend"/>
</svg>
```

A figure with several overlay annotations: a [`<s:halo>`](figure-overlay-components#halo) and [`<s:callout>`](figure-overlay-components#callout) highlighting the center, a dashed [`<s:roi-rect>`](figure-overlay-components#roi-rectangle) bounding box, and an [`<s:arrow>`](figure-overlay-components#straight-arrows) trend line.

:::
Center Region A Trend

Figure 14: A figure with several overlay annotations: a <s:halo> and <s:callout> highlighting the center, a dashed <s:roi-rect> bounding box, and an <s:arrow> trend line.

Executable figure overlays

Overlays also work on figures whose visual content is generated by executable code. As with image-based figures, the SVG viewBox can use any coordinate system you prefer. Matching the dimensions of the rendered output is often the easiest approach because it lets you reason in output pixels and tends to be the clearest choice when aligning annotations with a chart or plot.

This example uses color-coded <s:callout> components with curved leader lines to annotate an R histogram. Each callout uses an explicit fill to distinguish the annotations:


::: figure

```r exec
hist(rnorm(1000), breaks=30, xlim=c(-4, 4), col='#b0d0e8', border='#7aaccc', xlab='Random normal variate', ylab='Frequency', main='')
```

```svg overlay
<svg viewBox="0 0 600 400" xmlns:s="https://stencila.io/svg">
  <s:callout x="460" y="45" label="Peak near μ=0" to-x="335" to-y="100" curve="quad" fill="crimson"/>
  <s:callout x="105" y="130" label="Thin left tail" to-x="185" to-y="240" curve="quad" fill="darkorange"/>
  <s:callout x="410" y="190" label="Thin right tail" to-x="445" to-y="270" curve="quad" fill="seagreen"/>
</svg>
```

An R histogram annotated with overlay callouts — the kind of annotation that is easy with components but would require manual arrowhead definitions and path geometry in raw SVG.

:::
Peak near μ=0 Thin left tail Thin right tail

Figure 15: An R histogram annotated with overlay callouts — the kind of annotation that is easy with components but would require manual arrowhead definitions and path geometry in raw SVG.

Overlays also work with browser-side rendering libraries like Plotly, eCharts, and Vega-Lite. However, because these libraries can resize their output dynamically (for example, to fill a container or respond to a window resize), overlay alignment is currently most reliable when you set explicit width and height in the chart's layout options and use a matching SVG viewBox. This is a practical workaround for dynamically sized chart output, not a general requirement of SVG overlays.


::: figure

```plotly exec
{
  "data": [
    {
      "type": "scatter",
      "mode": "lines",
      "x": ["Jan", "Feb", "Mar", "Apr", "May"],
      "y": [0, 1.5, 5.2, 8, 6]
    }
  ],
  "layout": {
    "width": 600,
    "height": 400,
    "xaxis": {"title": {"text": "Month"}},
    "yaxis": {"title": {"text": "Value"}}
  }
}
```

```svg overlay
<svg viewBox="0 0 600 400" xmlns:s="https://stencila.io/svg">
  <s:halo cx="422" cy="25" r="16"/>
  <s:callout x="460" y="25" label="Peak"/>
  <s:arrow x="170" y="240" to-x="238" to-y="165" stroke="green" stroke-width="3" label="Rising trend" label-position="below" fill="green"/>
</svg>
```

A Plotly chart with an overlay highlighting the peak with a [`<s:halo>`](figure-overlay-components#halo) and [`<s:callout>`](figure-overlay-components#callout), and an [`<s:arrow>`](figure-overlay-components#straight-arrows) indicating the rising trend.

:::
Peak Rising trend

Figure 16: A Plotly chart with an overlay highlighting the peak with a <s:halo> and <s:callout>, and an <s:arrow> indicating the rising trend.

Note

Overlay alignment with dynamically-sized browser-rendered charts is a known limitation. We are working on improvements to make overlays track resizable content automatically. In the meantime, pinning the chart dimensions in its layout configuration is the most reliable approach.

Subfigures with individual overlays

Each subfigure can carry its own overlay. The overlay on each panel is scoped to that subfigure's content area. Here an <s:roi-rect> highlights a rectangular region on the left panel and an <s:roi-ellipse> highlights an elliptical region on the right:


::: figure [2]

    ::: figure

    ![](images/400x250-purple.png)

    ```svg overlay
    <svg viewBox="0 0 400 250" xmlns:s="https://stencila.io/svg">
      <s:roi-rect x="50" y="40" width="140" height="90" label="Feature α" label-position="below"/>
    </svg>
    ```

    Purple panel.

    :::

    ::: figure

    ![](images/400x250-teal.png)

    ```svg overlay
    <svg viewBox="0 0 400 250" xmlns:s="https://stencila.io/svg">
      <s:roi-ellipse cx="280" cy="130" rx="80" ry="50" label="Feature β" label-position="below"/>
    </svg>
    ```

    Teal panel.

    :::

Two subfigures, each with its own overlay annotation.

:::
Feature α
Feature β

Figure 17: Two subfigures, each with its own overlay annotation. (A) Purple panel. (B) Teal panel.

Parent overlay connecting subfigures

A top-level figure with subfigures can also carry its own overlay. Because this overlay sits above the entire grid, it can annotate relationships between panels — for example, drawing an arrow from a feature in one subfigure to a corresponding feature in another. Each subfigure can still have its own overlay alongside the parent one.

On small screens the grid collapses to a vertical stack, and when that happens the parent overlay is automatically hidden. The grid layout that the overlay was designed for no longer exists, so its coordinates would be meaningless. Subfigure overlays are unaffected — each one is scoped to its own panel and continues to scale correctly.

In this example the parent overlay draws a connecting <s:arrow> between the two panels, while each subfigure uses its own overlay — a <s:halo> with <s:callout> on the left and an <s:roi-rect> on the right. The parent overlay uses figure-level coordinates rather than either panel's local pixel coordinates, so its viewBox describes the overall two-panel layout rather than the dimensions of an individual image:


::: figure [2]

    ::: figure

    ![](images/400x250-purple.png)

    ```svg overlay
    <svg viewBox="0 0 400 250" xmlns:s="https://stencila.io/svg">
      <s:halo cx="200" cy="125" r="40"/>
      <s:callout x="180" y="210" label="Source"/>
    </svg>
    ```

    Left panel with source.

    :::

    ::: figure

    ![](images/400x250-teal.png)

    ```svg overlay
    <svg viewBox="0 0 400 250" xmlns:s="https://stencila.io/svg">
      <s:roi-rect x="120" y="70" width="100" height="100" label="Target" label-position="below"/>
    </svg>
    ```

    Right panel with target.

    :::

```svg overlay
<svg viewBox="0 0 800 250" xmlns:s="https://stencila.io/svg">
  <s:arrow x="350" y="150" to-x="500" to-y="150" label="Leads to" label-position="above"/>
</svg>
```

Subfigures with individual annotations plus a parent overlay arrow connecting them.

:::
Source
Target
Leads to

Figure 18: Subfigures with individual annotations plus a parent overlay arrow connecting them. (A) Left panel with source. (B) Right panel with target.

Padding

Padding adds whitespace around a figure's content area. For overlays, that extra space becomes part of the overlay coordinate space, which lets you place arrows, labels, notes, or scale bars just outside the image instead of drawing over it.

Add padding on the figure fence using a pad attribute:


::: figure {pad=50}

::: figure {pad="30 60"}

::: figure [2] {pad="10 20 30 40"}
  • 50 means 50px on all sides.

  • 30 60 means 30px top and bottom, 60px left and right.

  • 10 20 30 40 means top, right, bottom, left.

When using more than one padding value, quote the value so it is parsed as a single attribute.

How padding affects overlay coordinates

Padding extends the overlay coordinate space. For an image that is W×H pixels with pad="T R B L", the overlay viewBox should be "0 0 (W+L+R) (H+T+B)" and the image top-left sits at coordinate (L, T). This means all image-space coordinates shift right by L and down by T.

When top and left padding are both zero (the most common case — adding space below or to the right), image coordinates do not change. For example, {pad="0 0 56 0"} on a 600×300 image gives viewBox="0 0 600 356" and the image still starts at (0, 0). But {pad="50 20 56 20"} on the same image gives viewBox="0 0 640 406" with the image top-left at (20, 50), so an annotation that was at x="100" y="80" in the unpadded image needs to move to x="120" y="130".

Padding for a single figure overlay

This example adds bottom padding so a <s:scale-bar> can sit below the image instead of covering it:


::: figure {pad="0 0 56 0"}

![](images/600x300-teal.png)

```svg overlay
<svg viewBox="0 0 600 356" xmlns:s="https://stencila.io/svg">
  <s:scale-bar x="40" y="326" length="130" label="20 μm"/>
</svg>
```

A figure with bottom padding that leaves room for a scale bar beneath the image.

:::
20 μm

Figure 19: A figure with bottom padding that leaves room for a scale bar beneath the image.

Padding on a parent figure

Padding also works on parent figures with grid layouts. Here the parent figure uses bottom padding so <s:callout> leader lines can point into each panel from a shared label below the grid:


::: figure [2] {pad="0 0 56 0"}

    ::: figure

    ![](images/400x250-purple.png)

    Purple panel.

    :::

    ::: figure

    ![](images/400x250-blue.png)

    Blue panel.

    :::

```svg overlay
<svg viewBox="0 0 824 306" xmlns:s="https://stencila.io/svg">
  <s:callout x="370" y="290" label="Points of interest" to-x="220" to-y="210"/>
  <s:callout x="370" y="290" label="" to-x="610" to-y="90"/>
</svg>
```

A two-panel figure with parent padding that leaves room for a note below the grid.

:::
Points of interest

Figure 20: A two-panel figure with parent padding that leaves room for a note below the grid. (A) Purple panel. (B) Blue panel.

© 2026 Stencila