Skip to content

Image: Show captions inside the lightbox - #81500

Open
jasmussen wants to merge 7 commits into
trunkfrom
try/lightbox-captions
Open

jasmussen wants to merge 7 commits into
trunkfrom
try/lightbox-captions

Conversation

@jasmussen

@jasmussen jasmussen commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

What?

Closes #56587.

The lightbox ("Enlarge on click") duplicates the image into a shared overlay but has always dropped the caption. This PR carries the caption into the lightbox for single images and gallery images, with identical handling for both.

state

The image is not moved. A short caption sits just below/over the image; a long caption scrolls up and covers the (fixed) image, and you scroll back up to reveal it again.

Alignment inherits from theme.json global styles.elements.caption (via the wp-element-caption class) — no alignment is hardcoded. Width inherits the theme content width (--wp--style--global--content-size, i.e. theme.json settings.layout.contentSize), capped at 90vw, falling back to 480px when a theme sets none.
The panel is painted with the overlay background at 80% (color-mix) so it stays legible while the covered slice of image shows through.

The way this works is: Increase the overall safe area around the whole image, so that the caption doesn't need to cover part of it to be visible. If we do this, we can keep the opacity of the caption 100%. That will visually look much closer to the "external caption" design proposed in #56587.

We should pick one of those paths. Which one resonates?

Why?

Captions in galleries are especially useful on mobile where you might open and swipe, and if they simply disappear as they do now, we're missing an opportunity.

Testing Instructions

Add an Image block, add a caption, and enable Enlarge on click (link toolbar). Confirm the caption appears in the lightbox.
Repeat inside a Gallery; page through images and confirm each caption follows.
Give one image a very long caption and confirm it scrolls up over the fixed image; a short one should need no scroll.
Try an image with no caption — behavior should be unchanged from today.

Use of AI Tools

Claude Opus 4.8

@jasmussen
jasmussen requested a review from a team August 12, 2026 11:28
@jasmussen jasmussen self-assigned this Aug 12, 2026
@jasmussen jasmussen added [Type] Enhancement A suggestion for improvement. [Block] Image Affects the Image Block [Block] Gallery Affects the Gallery Block - used to display groups of images labels Aug 12, 2026
@github-actions github-actions Bot added the [Package] Block library /packages/block-library label Aug 12, 2026
@github-actions

github-actions Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Size Change: +1.19 kB (+0.02%)

Total Size: 7.73 MB

📦 View Changed
Filename Size Change
build/modules/block-library/image/view.min.js 2.7 kB +59 B (+2.23%)
build/styles/block-library/image/style-rtl.css 3.1 kB +149 B (+5.05%) 🔍
build/styles/block-library/image/style-rtl.min.css 2.01 kB +143 B (+7.66%) 🔍
build/styles/block-library/image/style.css 3.11 kB +150 B (+5.06%) 🔍
build/styles/block-library/image/style.min.css 2 kB +142 B (+7.64%) 🔍
build/styles/block-library/style-rtl.css 22.6 kB +142 B (+0.63%)
build/styles/block-library/style-rtl.min.css 19.1 kB +131 B (+0.69%)
build/styles/block-library/style.css 22.7 kB +146 B (+0.65%)
build/styles/block-library/style.min.css 19.1 kB +131 B (+0.69%)

compressed-size-action

@fcoveram

Copy link
Copy Markdown
Contributor

Nice ✨ Thanks for working on this.

I like the idea, but I would go with option 2:

Increase the overall safe area around the whole image, so that the caption doesn't need to cover part of it to be visible. If we do this, we can keep the opacity of the caption 100%.

I'm trying it with a simple caption text, and the outcome doesn't look good. The design needs to be more opinionated.

CleanShot 2026-08-13 at 11 09 17@2x

In that vein, I also wouldn't assume that users want to show the caption info in lightbox. Clicking. the image to enlarge it is an explicit action just for the image. That option could be a toggle in Inspector's settings tab that says

[On/Off] Show caption in enlarged view

@jasmussen

Copy link
Copy Markdown
Contributor Author

Changed based on your feedback, can you give it another look? I wonder if the safe area should be slightly larger still? Note that on mobile, this is problematic as that's where you need the extra space the most. We can make the spacing asymmetric, but not sure if that would look good, because note that this safe area change affects every image, including those without captions:

state

In that vein, I also wouldn't assume that users want to show the caption info in lightbox. Clicking. the image to enlarge it is an explicit action just for the image. That option could be a toggle in Inspector's settings tab that says

Speaking of opinionated, I'd love to see if we can start without that. I feel like if one state has captions, the other should too. That's not to completely invalidate your idea, just that I'm wondering if we can start with seeing if this PR is able to land, and then if we receive feedback otherwise, we can proceed and add a toggle at that point. What do you think?

@fcoveram

fcoveram commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

It looks good to me now. Although I would apply the same spacing on the sides of image and caption so both are aligned.

CleanShot 2026-08-14 at 11 43 54@2x

I wonder if the safe area should be slightly larger still? Note that on mobile, this is problematic as that's where you need the extra space the most.

The spacing you're applying looks good for a single line of text, which, to me, is an opinionated design choice for an image caption.

I feel like if one state has captions, the other should too.

Agree only if it applies to the gallery, not to individual images that might be placed all over the document.

…we can start with seeing if this PR is able to land, and then if we receive feedback otherwise, we can proceed and add a toggle at that point. What do you think?

That sounds good to me.

@jasmussen

Copy link
Copy Markdown
Contributor Author

Forgot to mention, but I applied your feedback!

@fcoveram

Copy link
Copy Markdown
Contributor

I don't see new commits. Is that correct?

@jasmussen

Copy link
Copy Markdown
Contributor Author

Okay now I pushed changes.

jasmussen and others added 7 commits October 22, 2026 11:08
The lightbox ("Enlarge on click") duplicates the image into a shared
overlay but previously dropped the caption. Carry the caption over so it
appears in the lightbox, for single images and for gallery images.

- Extract the rendered `<figcaption>` in `block_core_image_render_lightbox()`,
  sanitize with `wp_kses_post()`, and store it in the interactivity state
  (`caption`). Reading the rendered content covers all sources with one
  path: single images, static galleries, and dynamic galleries whose
  caption comes from the attachment's `post_excerpt`.
- Add a caption element to the shared overlay markup, injected via a
  `data-wp-watch` callback (a `data-wp-bind--innerHTML` binding does not
  work: Preact reconciles children and won't apply a plain `innerHTML`
  prop). The watch re-applies on gallery navigation.
- Inherit caption alignment from theme.json global `styles.elements.caption`
  via the `wp-element-caption` class; no `text-align` is hardcoded.

Placeholder positioning only; final caption design is a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lay the caption in a scroll layer over the (unchanged) enlarged image. A
short caption sits in the space below the image; a long caption scrolls
up and covers the image, and scrolling back up reveals it again. The
image never moves.

- `.wp-lightbox-caption-scroll` overlays the image (overflow-y:auto,
  overscroll-behavior:contain). A transparent spacer sized to
  `calc(50vh + var(--wp--lightbox-container-height)/2)` anchors the
  caption to the image's bottom edge; because 50vh reflows and the height
  var is republished by `setOverlayStyles()` on window resize, the anchor
  stays correct responsively.
- The whole layer is hidden when the image has no caption, so those
  images keep the exact shipping behavior.
- `actions.handleCaptionClick` keeps clicks inside the caption (text
  selection, links) from bubbling to the overlay's close handler; the
  image and surrounding area still close the lightbox.
- Caption measure inherits the theme's content width
  (`--wp--style--global--content-size`, fallback 480px, capped at 90vw).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Start the caption panel 32px above the image's bottom edge so a short
caption is visible without scrolling, and paint it with the overlay
background at 80% (via color-mix) so it reads clearly while the part of
the image it overlaps still shows through. Restore 24px block padding.

Kept deliberately small so it can be reverted independently if the
translucent-overlap direction isn't wanted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Supersedes the translucent-overlap approach after design feedback. Rather
than lifting the caption over the image with a translucent panel, reserve
extra breathing room around the enlarged image so the caption has space to
sit below it.

- `setOverlayStyles()` now adds a 24px inset on every side of the enlarged
  image, applied to all images (not only captioned ones) so navigating a
  gallery never resizes the image.
- The caption anchors at the image's bottom edge again (no overlap), and
  the panel background is fully opaque, so a caption long enough to scroll
  cleanly covers the image as it moves up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#79114 moved the lightbox's close click off the overlay root onto
`.scrim`, and made `handleTouchMove()` call `preventDefault()` on every
touch gesture. Both assumptions had been baked into the caption layer.

Keep the caption layer out of the way of pointer events except on the
panel itself, so background clicks still reach `.scrim` and close the
lightbox. Leave gestures that start on the caption to scroll natively
rather than preventing them and tracking them as a swipe. Drop
`handleCaptionClick()`, which only existed to stop clicks reaching the
overlay's old close handler.

Also move the changelog entry to the current Unreleased section, where
the rebase did not carry it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The caption layer takes no pointer events outside the caption panel, so
that background clicks still reach `.scrim` and gallery swipes still
reach the overlay. On a pointer device that left the caption all but
unreachable: the panel only begins below the enlarged image, which fills
most of the overlay, so a wheel almost always landed somewhere that
scrolled the page behind instead — which `handleScroll()` then snapped
straight back, so nothing moved.

Carry those deltas over to the caption by hand, and prevent the scroll
they would otherwise have caused. A wheel over the panel already scrolls
it natively and is left alone, as is a `ctrl`-wheel, which zooms the
page, and an image with no caption, where no scroll layer exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jasmussen
jasmussen force-pushed the try/lightbox-captions branch from 7f14d46 to ff105b8 Compare October 22, 2026 09:57
@jasmussen

Copy link
Copy Markdown
Contributor Author

I've rebased this branch, to pick up everything it was blocked on before. It's now using the same fundamental structure as #79114 moved towards:

lightbox captions mobile lightbox captions desktop

I discovered a bug in trunk in the process which I'll push a separate fix for, but this branch is not blocked on it.

@jasmussen
jasmussen marked this pull request as ready for review October 22, 2026 09:59
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

🎉 Props

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: jasmussen <joen@git.wordpress.org>
Co-authored-by: fcoveram <fcoveram@git.wordpress.org>
Co-authored-by: t-hamano <wildworks@git.wordpress.org>
Co-authored-by: james-tyner <jtyner@git.wordpress.org>
Co-authored-by: dknauss <dpknauss@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Updated as activity occurs, without notifying anyone named here. Add the props-bot label to refresh.

📦 Bundle size

Size Change: +1.38 kB (+0.02%)

Total Size: 9.05 MB

📦 View Changed
Filename Size Change
build/modules/block-library/image/view.min.js 3.63 kB +168 B (+4.85%) 🔍
build/styles/block-library/image/style-rtl.css 3.17 kB +166 B (+5.53%) 🔍
build/styles/block-library/image/style-rtl.min.css 2.1 kB +156 B (+8.01%) 🔍
build/styles/block-library/image/style.css 3.18 kB +167 B (+5.54%) 🔍
build/styles/block-library/image/style.min.css 2.09 kB +153 B (+7.89%) 🔍
build/styles/block-library/style-rtl.css 23 kB +144 B (+0.63%)
build/styles/block-library/style-rtl.min.css 19.5 kB +141 B (+0.73%)
build/styles/block-library/style.css 23.1 kB +143 B (+0.62%)
build/styles/block-library/style.min.css 19.5 kB +141 B (+0.73%)

ff105b8 Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric 3ff3081 trunk % Change
timeToFirstByte 42.95 ms +6.98% -3.73% 42.15 ms +4.27% -1.42% 1.9%
largestContentfulPaint 74 ms +5.41% -2.7% 72 ms +11.11% -2.78% 2.78%
lcpMinusTtfb 30.35 ms +12.69% -7.58% 29.55 ms +13.71% -5.08% 2.71%
wpBeforeTemplate 21.91 ms +6.71% -3.61% 21.74 ms +3.68% -3.86% 0.78%
wpTemplate 17.16 ms +5.24% -4.55% 17 ms +6.18% -4.47% 0.94%
wpTotal 39.5 ms +6.25% -3.92% 38.67 ms +4.63% -1.5% 2.15%
wpMemoryUsage 7.66 MB +0% -0% 7.63 MB +0% -0% 0.42%
wpDbQueries 16 +0% -0% 16 +0% -0% 0%

front-end-classic-theme

Metric 3ff3081 trunk % Change
timeToFirstByte 35.65 ms +0.98% -4.21% 35.1 ms +8.69% -1.71% 1.57%
largestContentfulPaint 80 ms +0% -0% 80 ms +7.5% -0% 0%
lcpMinusTtfb 44.25 ms +3.05% -0.79% 45.15 ms +2.33% -3.32% -1.99%
wpBeforeTemplate 18.81 ms +2.07% -1.22% 18.82 ms +9.51% -1.59% -0.05%
wpTemplate 13.52 ms +4.81% -5.77% 13.36 ms +8.46% -2.54% 1.2%
wpTotal 32.75 ms +1.25% -4.03% 32.28 ms +8.95% -2.26% 1.46%
wpMemoryUsage 6.35 MB +0% -0% 6.32 MB +0% -0% 0.53%
wpDbQueries 13 +0% -0% 13 +0% -0% 0%

media-processing

Metric 3ff3081 trunk % Change
mediaProcessingJpeg 339.37 ms +0.72% -0.75% 336.27 ms +0.73% -0.52% 0.92%
mediaProcessingAvif 4970.14 ms +0.85% -0.39% 4947.42 ms +0.92% -0.54% 0.46%
mediaProcessingJpegToAvif 3365.35 ms +0.45% -0.05% 3370.67 ms +0.69% -0.27% -0.16%

media-upload

Metric 3ff3081 trunk % Change
jpegUploadProcessing 1459.91 ms +32.25% -1.93% 1414.12 ms +1.05% -0.58% 3.24%
pngUploadProcessing 210 ms +7.33% -2.34% 218.02 ms +3.32% -3.85% -3.68%
largeJpegUploadProcessing 1415.78 ms +1.22% -0.39% 1421.04 ms +0.19% -0.95% -0.37%
multipleImageUploadProcessing 1536.34 ms +1.2% -1.6% 1516.68 ms +0.96% -0.64% 1.3%

post-editor

Metric 3ff3081 trunk % Change
serverResponse 528.36 ms +3.1% -2.77% 517.16 ms +7.64% -3.49% 2.17%
firstPaint 515.54 ms +1.99% -38.68% 501.39 ms +6.32% -35.07% 2.82%
domContentLoaded 1208.09 ms +6.02% -2.59% 1226.18 ms +2.04% -2.06% -1.48%
loaded 1209.5 ms +6.01% -2.61% 1227.6 ms +2.03% -2.09% -1.47%
firstContentfulPaint 531.05 ms +4.01% -2.48% 527.85 ms +3.18% -6.56% 0.61%
firstBlock 3706.42 ms +2.9% -3.82% 3639.48 ms +4.57% -1.59% 1.84%
type 19.63 ms +2.6% -7.39% 19.88 ms +4.88% -7.19% -1.26%
typeWithoutInspector 19.82 ms +3.78% -5.05% 18 ms +7.67% -2.17% 10.11%
typeWithTopToolbar 23.85 ms +10.61% -5.74% 24.33 ms +3.04% -4.85% -1.97%
typeContainer 8.96 ms +7.14% -5.47% 8.72 ms +10.21% -3.21% 2.75%
focus 136.55 ms +4.45% -2.97% 127.79 ms +3.41% -5.65% 6.85%
firstFocus 285.63 ms +0% -0% 225.84 ms +0% -0% 26.47%
selectAll 584.66 ms +1.96% -3.05% 578.73 ms +1.47% -1.52% 1.02%
listViewOpen 84.35 ms +7.21% -5.54% 81.01 ms +7.33% -10.42% 4.12%
inserterOpen 42.66 ms +7.36% -9.31% 40.37 ms +21.6% -2.97% 5.67%
inserterHover 12.61 ms +6.98% -7.77% 12.55 ms +3.19% -9.16% 0.48%
inserterSearch 8.73 ms +6.53% -8.71% 8.8 ms +10.45% -14.55% -0.8%
loadPatterns 673.92 ms +4.25% -1.67% 652.48 ms +4.15% -4.19% 3.29%
wpTotal 517.86 ms +3.15% -2.87% 506.21 ms +7.5% -3.46% 2.3%
wpMemoryUsage 14.70 MB +0% -0% 14.66 MB +0% -0% 0.24%
wpDbQueries 55 +0% -1.82% 55 +0% -1.82% 0%

site-editor

Metric 3ff3081 trunk % Change
serverResponse 277.39 ms +4.38% -5.68% 279.45 ms +2.86% -5.15% -0.74%
firstPaint 199.52 ms +9.37% -6.03% 230.4 ms +27.31% -22.7% -13.4%
domContentLoaded 871.01 ms +2.47% -2.46% 878.56 ms +1.44% -1.77% -0.86%
loaded 871.89 ms +2.46% -2.47% 879.49 ms +1.43% -1.77% -0.86%
firstContentfulPaint 385.84 ms +2.19% -1.87% 384.83 ms +4.1% -3.58% 0.26%
firstBlock 3152.34 ms +2.29% -1.21% 3228.49 ms +0.6% -0.56% -2.36%
type 15.18 ms +6.98% -1.71% 15.79 ms +8.42% -4.75% -3.86%
navigate 88.95 ms +7.42% -3.82% 103.8 ms +15.67% -13.65% -14.31%
loadPatterns 946.5 ms +7.8% -3.95% 1030.11 ms +17.33% -14.61% -8.12%
loadPages 994.09 ms +5.12% -0.61% 1020.73 ms +2.16% -1.36% -2.61%
wpTotal 270.44 ms +4.5% -5.95% 272.32 ms +1.93% -5.25% -0.69%
wpMemoryUsage 12.71 MB +0% -0% 12.68 MB +0% -0% 0.27%
wpDbQueries 44.5 +1.12% -1.12% 44 +2.27% -0% 1.14%

ff105b8 Run

🏁 Flaky tests

Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

Regression: updating a specific option will only change its value and will not corrupt others in /test/e2e/specs/editor/various/core-settings.spec.js, passed after 1 failed attempt.
Error: expect(received).toStrictEqual(expected) // deep equality

- Expected  - 1
+ Received  + 1

  Array [
    "rewrite_rules",
-   "SERIALIZED DATA",
+   "",
  ]
    at forEach (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/core-settings.spec.js:46:26)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/core-settings.spec.js:43:35
Undo restores the post selection when another synced entity is loaded (RTC WebSocket) in /test/e2e/specs/editor/collaboration/collaboration-undo-redo.spec.ts, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 2
+ Received  + 2

  Object {
    "attributeKey": "content",
    "blockIndex": 0,
    "content": "abcdef",
-   "endOffset": 3,
-   "offset": 3,
+   "endOffset": 2,
+   "offset": 2,
  }

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/collaboration/collaboration-undo-redo.spec.ts:363:5
refuses the drop and uploads nothing in /test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js, passed after 1 failed attempt.
Error: expect(received).toHaveLength(expected)

Expected length: 0
Received length: 1
Received array:  [{"_links": {"about": [{"href": "http://localhost:8889/wp-json/wp/v2/types/attachment"}], "author": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/users/1"}], "collection": [{"href": "http://localhost:8889/wp-json/wp/v2/media"}], "curies": [{"href": "https://api.w.org/{rel}", "name": "wp", "templated": true}], "replies": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/comments?post=223"}], "self": [{"href": "http://localhost:8889/wp-json/wp/v2/media/223", "targetHints": {"allow": ["GET", "POST", "PUT", "PATCH", "DELETE"]}}], "wp:attached-to": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/posts/222", "id": 222, "post_type": "post"}]}, "alt_text": "", "author": 1, "caption": {"rendered": ""}, "class_list": ["post-223", "attachment", "type-attachment", "status-inherit", "hentry", "entry"], "comment_status": "open", "date": "2026-10-22T10:14:23", "date_gmt": "2026-10-22T10:14:23", "description": {"rendered": "<p class=\"attachment\"><a href='http://localhost:8889/wp-content/uploads/2026/10/b0397814-2a2d-4b1e-ac6a-f90b83af9c74.png'><img loading=\"lazy\" decoding=\"async\" width=\"10\" height=\"10\" src=\"http://localhost:8889/wp-content/uploads/2026/10/b0397814-2a2d-4b1e-ac6a-f90b83af9c74.png\" class=\"attachment-medium size-medium\" alt=\"\" style=\"width:100%;height:100%;max-width:10px;\" /></a></p>
"}, "featured_media": 0, "filename": "b0397814-2a2d-4b1e-ac6a-f90b83af9c74.png", "filesize": 80, "guid": {"rendered": "http://localhost:8889/wp-content/uploads/2026/10/b0397814-2a2d-4b1e-ac6a-f90b83af9c74.png"}, "id": 223, "link": "http://localhost:8889/?attachment_id=223", "media_details": {"file": "2026/10/b0397814-2a2d-4b1e-ac6a-f90b83af9c74.png", "filesize": 80, "height": 10, "image_meta": {"alt": "", "aperture": "0", "camera": "", "caption": "", "copyright": "", "created_timestamp": "0", "credit": "", "focal_length": "0", "iso": "0", "keywords": [], "orientation": "0", "shutter_speed": "0", "title": ""}, "sizes": {}, "width": 10}, "media_type": "image", "meta": [], "mime_type": "image/png", "modified": "2026-10-22T10:14:23", "modified_gmt": "2026-10-22T10:14:23", "ping_status": "closed", "post": 222, "slug": "b0397814-2a2d-4b1e-ac6a-f90b83af9c74", "source_url": "http://localhost:8889/wp-content/uploads/2026/10/b0397814-2a2d-4b1e-ac6a-f90b83af9c74.png", "status": "inherit", "template": "", "title": {"rendered": "b0397814-2a2d-4b1e-ac6a-f90b83af9c74"}, "type": "attachment"}]
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js:71:45

ff105b8 Run

@jasmussen

Copy link
Copy Markdown
Contributor Author

The bug I mentioned, which pairs well with this PR: #85381.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Block] Gallery Affects the Gallery Block - used to display groups of images [Block] Image Affects the Image Block [Package] Block library /packages/block-library [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gallery captions: Enhance with theme colors, possibly captions outside option

2 participants