I have:
Bug description
In HTML output, combining cap-location: margin with a left-extending column
(column: page-left, column: screen-inset-left, column: screen-left, …) on a figure
places the caption in the correct margin columns but in the next grid row, so it appears
below the figure instead of aligned with its top.
Tables are not affected: with the same column class, a table's margin caption is correctly
top-aligned. The asymmetry comes from where the column-* class ends up in the DOM (details below).
Possibly related: #4150, #8136, #3829.
Steps to reproduce
No engine needed; image.png can be any image.
---
title: "Margin caption test"
format:
html:
cap-location: margin
tbl-cap-location: margin
---
::: {#fig-left .column-page-left}

Figure in `column-page-left`: caption is in the margin, but below the figure.
:::
{#fig-body}
::: {#tbl-left .column-page-left}
| a | b |
|---|---|
| 1 | 2 |
Table in `column-page-left`: caption is correctly top-aligned in the margin.
:::
The same happens with a computational cell (#| column: page-left + #| fig-cap) and with
column: screen-inset-left.
Actual behavior
The caption is in the margin columns but shifted down by the height of the figure row.
Measured bounding boxes (Chromium 141, viewport 1600×1200, quarto 1.10.18):
| element |
image x, y |
caption x, y |
#fig-left (column-page-left) |
232.5, 116.3 |
1152.5, 324.8 |
#fig-body (no column) |
527.5, 468.5 |
1152.5, 468.5 |
#tbl-left (column-page-left) |
232.5, 745.0 |
1152.5, 736.5 |
Caption y should match the figure y in the first row, as it does in rows 2 and 3.
Diagnosis
Generated markup for the figure case:
<figure class="quarto-float quarto-float-fig figure page-columns page-full">
<div aria-describedby="…" class="page-columns page-full"> <!-- wrapper -->
<img … class="figure-img column-page-left">
</div>
<figcaption class="… margin-caption">Figure 1: …</figcaption>
</figure>
The <figure> is a page-columns grid with two children. The relevant rules
(src/resources/formats/html/bootstrap/_bootstrap-rules.scss) are:
.page-full { grid-column: screen-start / screen-end !important; }
.margin-caption { grid-column: body-end / page-end !important; }
So the wrapper <div> spans screen-start → screen-end, i.e. the entire first grid row, and
sparse auto-placement has to push the figcaption to row 2 — hence "in the margin, but below".
Two things combine to produce this:
- For figures, the
column-* class is attached to the innermost element (the <img>), whereas
for tables it stays on the outer #tbl-… container
(src/resources/filters/layout/columns.lua, add_column_classes_and_attributes).
ensureInGrid() in src/format/html/format-html-bootstrap.ts (~L1178) then walks up from the
column-* element and adds page-columns and page-full to every ancestor up to
#quarto-content. Because the class is on the <img>, the plain wrapper <div> inside
<figure> becomes page-full and consumes the whole row.
In the table case, ensureInGrid() starts above the float container, so the inner wrapper keeps the
default .page-columns > * span (body-content-start / body-content-end) and the caption fits in
the same row — which is why tables look right.
Right-extending columns don't show the symptom because the caption cannot be placed in the margin
there anyway (cf. #4150).
Workaround
Taking the wrapper out of the grid restores top alignment; the image keeps its exact position and
size, since the nested grid used the same column template:
figure:has(> figcaption.margin-caption) > div.page-full {
display: contents;
}
Verified to fix column: page-left and column: screen-inset-left, and to leave body figures,
column: page-right, column: margin figures (caption stays below the image, as intended),
bottom captions and tables unchanged, including below the lg/md breakpoints where margin
captions fall back into the body flow.
Expected behavior
The figure's margin caption starts at the top of the figure, as it does for a body figure and
as it does for a table in the same column.
Your environment
- Quarto 1.10.18, linux-amd64
- Chromium 141.0.7390.37 (also reproducible in any browser, the layout is pure CSS grid)
Quarto check output
% quarto check
Quarto 1.10.18
[✓] Checking environment information...
Quarto cache location: /Users/[...]/Library/Caches/quarto
[✓] Checking versions of quarto binary dependencies...
Pandoc version 3.10.0: OK
Dart Sass version 1.101.0: OK
Deno version 2.7.14: OK
Typst version 0.15.1: OK
[✓] Checking versions of quarto dependencies......OK
[✓] Checking Quarto installation......OK
Version: 1.10.18
Path: /Applications/Positron.app/Contents/Resources/app/quarto/bin
[✓] Checking tools....................OK
TinyTeX: (not installed)
Chrome Headless Shell: (not installed)
VeraPDF: (not installed)
[✓] Checking LaTeX....................OK
Using: Installation From Path
Path: /Library/TeX/texbin
Version: 2026
[✓] Checking Chrome Headless....................OK
Using: Chrome found on system
Path: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
Source: MacOS known location
[✓] Checking basic markdown render....OK
[✓] Checking R installation...........OK
Version: 4.5.2
Path: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources
LibPaths:
- /Users/[...]/Library/R/arm64/4.5/library
- /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
knitr: 1.50
rmarkdown: 2.30
[✓] Checking Knitr engine render......OK
[✓] Checking Python 3 installation....OK
Version: 3.14.0
Path: /Users/[...]/.venv/bin/python3
Jupyter: 5.9.1
Kernels: python3
(/) Checking Jupyter engine render....[IPKernelApp] WARNING | Kernel is running over TCP without encryption. All communication (including code and outputs) is sent in plain text and is susceptible to eavesdropping. Use IPC transport or launch with kernel manager-provisioned CurveZMQ keys to enable transport encryption.
[✓] Checking Jupyter engine render....OK
[✓] Checking Julia installation...
I have:
Bug description
In HTML output, combining
cap-location: marginwith a left-extending column(
column: page-left,column: screen-inset-left,column: screen-left, …) on a figureplaces the caption in the correct margin columns but in the next grid row, so it appears
below the figure instead of aligned with its top.
Tables are not affected: with the same column class, a table's margin caption is correctly
top-aligned. The asymmetry comes from where the
column-*class ends up in the DOM (details below).Possibly related: #4150, #8136, #3829.
Steps to reproduce
No engine needed;
image.pngcan be any image.The same happens with a computational cell (
#| column: page-left+#| fig-cap) and withcolumn: screen-inset-left.Actual behavior
The caption is in the margin columns but shifted down by the height of the figure row.
Measured bounding boxes (Chromium 141, viewport 1600×1200,
quarto 1.10.18):x,yx,y#fig-left(column-page-left)#fig-body(no column)#tbl-left(column-page-left)Caption
yshould match the figureyin the first row, as it does in rows 2 and 3.Diagnosis
Generated markup for the figure case:
The
<figure>is apage-columnsgrid with two children. The relevant rules(
src/resources/formats/html/bootstrap/_bootstrap-rules.scss) are:.page-full { grid-column: screen-start / screen-end !important; }.margin-caption { grid-column: body-end / page-end !important; }So the wrapper
<div>spansscreen-start → screen-end, i.e. the entire first grid row, andsparse auto-placement has to push the
figcaptionto row 2 — hence "in the margin, but below".Two things combine to produce this:
column-*class is attached to the innermost element (the<img>), whereasfor tables it stays on the outer
#tbl-…container(
src/resources/filters/layout/columns.lua,add_column_classes_and_attributes).ensureInGrid()insrc/format/html/format-html-bootstrap.ts(~L1178) then walks up from thecolumn-*element and addspage-columnsandpage-fullto every ancestor up to#quarto-content. Because the class is on the<img>, the plain wrapper<div>inside<figure>becomespage-fulland consumes the whole row.In the table case,
ensureInGrid()starts above the float container, so the inner wrapper keeps thedefault
.page-columns > *span (body-content-start / body-content-end) and the caption fits inthe same row — which is why tables look right.
Right-extending columns don't show the symptom because the caption cannot be placed in the margin
there anyway (cf. #4150).
Workaround
Taking the wrapper out of the grid restores top alignment; the image keeps its exact position and
size, since the nested grid used the same column template:
Verified to fix
column: page-leftandcolumn: screen-inset-left, and to leave body figures,column: page-right,column: marginfigures (caption stays below the image, as intended),bottom captions and tables unchanged, including below the
lg/mdbreakpoints where margincaptions fall back into the body flow.
Expected behavior
The figure's margin caption starts at the top of the figure, as it does for a body figure and
as it does for a table in the same column.
Your environment
Quarto check output