Skip to content

Gallery: Improve mobile lightbox with animated swiping - #79114

Open
jasmussen wants to merge 6 commits into
trunkfrom
try/sliding-lightbox-galleries
Open

jasmussen wants to merge 6 commits into
trunkfrom
try/sliding-lightbox-galleries

Conversation

@jasmussen

Copy link
Copy Markdown
Contributor

What?

Followup to #62906. Adds animation to the swiping gesture for mobile galleries:
animate galleries

Why?

When you have a gallery set to lightbox ("expand on click"), no mobile you can open it and use a swipe gesture to advance between images. However there is no visual feedgback that this is possible. This PR adds that.

Testing Instructions

Create a gallery, set to expand on click, then test on mobile, or using the web-inspector set to device emulation. Test swiping images to advance.

Use of AI Tools

Claude Opus 4.7.

@jasmussen jasmussen self-assigned this Jun 11, 2026
@jasmussen jasmussen added [Type] Enhancement A suggestion for improvement. [Block] Gallery Affects the Gallery Block - used to display groups of images labels Jun 11, 2026
@github-actions github-actions Bot added the [Package] Block library /packages/block-library label Jun 11, 2026
jasmussen added a commit that referenced this pull request Jun 11, 2026
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Size Change: +1.36 kB (+0.02%)

Total Size: 7.73 MB

📦 View Changed
Filename Size Change
build/modules/block-library/accordion/view.min.js 654 B +50 B (+8.28%) 🔍
build/modules/block-library/image/view.min.js 3.08 kB +432 B (+16.34%) ⚠️
build/styles/block-library/image/style-rtl.css 3.07 kB +119 B (+4.03%)
build/styles/block-library/image/style-rtl.min.css 1.97 kB +104 B (+5.57%) 🔍
build/styles/block-library/image/style.css 3.08 kB +121 B (+4.09%)
build/styles/block-library/image/style.min.css 1.97 kB +106 B (+5.7%) 🔍
build/styles/block-library/style-rtl.css 22.6 kB +109 B (+0.49%)
build/styles/block-library/style-rtl.min.css 19.1 kB +105 B (+0.55%)
build/styles/block-library/style.css 22.7 kB +116 B (+0.51%)
build/styles/block-library/style.min.css 19 kB +103 B (+0.54%)

compressed-size-action

jasmussen added a commit that referenced this pull request Jul 27, 2026
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jasmussen
jasmussen force-pushed the try/sliding-lightbox-galleries branch from c6eca7d to 022d8e4 Compare July 27, 2026 13:18
@jasmussen

Copy link
Copy Markdown
Contributor Author

Rebased, and worked in some small improvements.

@jasmussen
jasmussen requested a review from a team July 27, 2026 13:19
@github-actions

Copy link
Copy Markdown

Flaky tests detected in 022d8e4.
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.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/30269701069
📝 Reported issues:

jasmussen added a commit that referenced this pull request Jul 28, 2026
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jasmussen
jasmussen force-pushed the try/sliding-lightbox-galleries branch from 022d8e4 to 0b51fe4 Compare July 28, 2026 07:47
@fcoveram

Copy link
Copy Markdown
Contributor

It works well for me. It would be nicer if, after a certain number of pixels of swiping, the next/prev image is revealed to make the transition smoother and reinforce the swipe direction. Right now, the new image shows up slightly abruptly.

@jasmussen

Copy link
Copy Markdown
Contributor Author

Right now, the new image shows up slightly abruptly.

I agree. When I worked on that, this aspect was a bit of a larger undertaking, so in the hopes of making this a small iterative improvement, I chose this simpler approach. But it should be possible to upgrade it with a second PR. Curious if we can get a developer instinct on this, happy either way.

@jasmussen

Copy link
Copy Markdown
Contributor Author

Noting that this PR is now blocking a followup in #81500, as both need to touch mobile swiping.

@jasmussen

Copy link
Copy Markdown
Contributor Author

I'd love eyes on this one and #81500 if you have time, @WordPress/gutenberg-design, I think they'd make nice little 7.2 improvements.

@jameskoster

Copy link
Copy Markdown
Contributor

I had the same instinct as @fcoveram but it seems fine to handle separately.

@andrewserong andrewserong left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea! Left a couple of code quality / code style nits, but generally testing pretty well in the mobile view of Chrome.

One bug I ran into is with cancelled drag events. I haven't tested this on a real phone or tablet, but for drag events in Chrome you can hit escape to cancel a drag part way through. In this case, the state gets stuck and isn't cleared, so if you re-open the item you closed, it'll be stuck in its drag offset:

2026-10-08.17.26.13.mp4

Should we add an event handler for touch cancel?

One other thought (not a blocker for this PR) is that it'd be nice to have this behaviour for click drags as well and not just for touch devices (I know I want to click and drag things on desktop quite a bit, rather than just clicking next and back buttons). So, there's an opportunity to put some of the logic here into functions that could be re-used across the different event handlers. Again, that could be a follow-up, though if you were keen on getting this PR in, in the shorter-term without refactoring.

I'm wrapping up for the week now, but happy to give this a re-review / closer look next week if you'd like a hand with it!

Edit: one other note is that it seems is-fading-in sticks around after it concludes. Is that an issue, or is it fine? I couldn't see any visual issues with it lingering, just wondering if there's potential for it to conflict with other animations.

left: 50%;
transform-origin: top left;
transform: translate(-50%, -50%);
transform: translate(calc(-50% + var(--wp--lightbox-drag-offset, 0px)), -50%);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how much of an issue it is for this PR as it might become more apparent if/when we get to the point of showing the next / prev image while dragging, but just wondering if the transform drag offset belongs on the lightbox-image-container or on the img element?

(This could be looked at in a follow-up, so not a blocker for this PR)

Comment thread packages/block-library/CHANGELOG.md Outdated

- Playlist: Shorten the track toolbar button label from "Add track" to "Add".
- Gallery: Rename the dynamic variation's "Convert to images" action to "Detach", and confirm it in a dialog explaining that the gallery will keep its current images but stop updating automatically ([#80727](https://github.com/WordPress/gutenberg/pull/80727)).
- Image: Animate touch-swipe navigation in the gallery lightbox so the image follows the finger and slides off on commit, with a quick fade-in for the next image ([#79114](https://github.com/WordPress/gutenberg/pull/79114)).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny nit: this needs to be moved up to the unreleased section

*/
const touchDrag = {
isDragging: false,
direction: 'unknown',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'unknown' seems like a bit of an odd value for a default state. Why not null or undefined?

Comment on lines +382 to +384
touchDrag.isDragging = false;
touchDrag.direction = 'unknown';
touchDrag.overlayEl = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're repeating this clearing behaviour in a couple of places. Is it worth adding a function to handle resetting touchDrag to defaults? This might be useful for the bug with cancelling a drag (I'll comment on that separately).

jasmussen and others added 6 commits October 8, 2026 09:16
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ghtbox

The swipe commit animation read the media query inline. Trunk has since
moved `prefersReducedMotion()` into the block library's shared utils
(#84297), so use that instead of a second copy of the same query.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ipes

Addresses review feedback on #79114.

A drag the browser cancels — Escape during a drag in Chrome, or a system
gesture taking over — fires `touchcancel` instead of `touchend`, so the
drag offset was left on the overlay. The overlay is a singleton, so the
offset survived closing and reopening the lightbox and the image appeared
pushed off-centre. Reopening a different image hid it, because that
recomputes the overlay styles; reopening the same one did not. Handle
`touchcancel` and abandon the drag.

The reset was also spelled out in two places and did not cover the
styles, so collect it in `resetTouchDrag()`, `clearDragStyles()` and
`flushPendingSlide()`. Flushing rather than cancelling a pending slide
fixes a second stuck state: touching the overlay during the commit slide
dropped the timer that both swapped the image and cleared the offset,
leaving the image parked off-screen.

Take `is-fading-in` back off once the fade is over, so the class means
what it says. It outranks the `.zoom.active` rule that animates the image
in, though nothing restarts that animation while the overlay stays open,
and closing the lightbox clears the class along with the rest of the
rendered class list — so there is no visual bug to fix here, only a state
that should not be left set.

Also use `null` rather than 'unknown' for the un-latched drag direction,
and move the changelog entry to the unreleased section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Taking `is-fading-in` off once the fade was over restarted the zoom-up
animation the lightbox plays when an image is first opened: the
`.zoom.active` rule declares that animation on the image container for as
long as the overlay is open, and the fade rule only shadows it. Removing
the class changes the container's computed `animation-name` back, which
the browser treats as a new animation and runs from the start — so a
swipe finished with the next image fading in, blinking out, and zooming
up from the gallery as if it had just been opened.

An animationstart trace over one committed swipe shows the fade at 245ms
and `lightbox-zoom-in` at 386ms; pinning the class on removes the second
entry. So leave the class on, as it was before, and say why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The swipe used a CSS transition and a CSS animation, switched on by
classes toggled on the overlay, with a `setTimeout` waiting out durations
that had to be kept in step with `style.scss` by comment. That coupling
produced both of the bugs in this branch: toggling a class changed the
image container's computed `animation-name`, which restarted the
open-zoom animation, and a stale timer could strand the image off-screen.

Animate the slide and the fade with `element.animate()` instead. Scripted
animations take precedence over an element's CSS animations without
touching its class list, so the open zoom the lightbox plays — rebuilt in
#79058 — is left entirely alone, and the swipe cannot restart it. The
durations now live only in `view.js`; `is-animating-slide`,
`is-fading-in` and their keyframes are gone from the stylesheet.

`animation.cancel()` replaces the hand-rolled abort, and settling a swipe
that is still in flight replaces the flush, so an interrupted or
cancelled gesture has one way to end rather than several.

The drag offset also moves from the overlay to the image containers. The
overlay's `style` attribute is bound to `state.overlayStyles`, and the
Interactivity API assigns a bound style string to `cssText`, so a resize
part way through a gesture rewrote the attribute and wiped the drag out
from under the finger. Nothing is bound to the containers.

Verified in a gallery lightbox: no CSS animation starts at all during a
swipe, `lightbox-zoom-in` still plays on open and reopen, a drag survives
a viewport change mid-gesture, and cancelled, interrupted and uncommitted
gestures all return the image to rest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jasmussen
jasmussen force-pushed the try/sliding-lightbox-galleries branch from c4ee325 to 4de181b Compare October 8, 2026 08:11
@github-actions

github-actions Bot commented Oct 8, 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: andrewserong <andrewserong@git.wordpress.org>
Co-authored-by: fcoveram <fcoveram@git.wordpress.org>
Co-authored-by: jameskoster <jameskoster@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: +767 B (+0.01%)

Total Size: 8.3 MB

📦 View Changed
Filename Size Change
build/modules/block-library/image/view.min.js 3.35 kB +631 B (+23.2%) 🚨
build/styles/block-library/image/style-rtl.css 3 kB +15 B (+0.5%)
build/styles/block-library/image/style-rtl.min.css 1.95 kB +16 B (+0.83%)
build/styles/block-library/image/style.css 3.01 kB +17 B (+0.57%)
build/styles/block-library/image/style.min.css 1.94 kB +16 B (+0.83%)
build/styles/block-library/style-rtl.css 22.7 kB +17 B (+0.07%)
build/styles/block-library/style-rtl.min.css 19.3 kB +20 B (+0.1%)
build/styles/block-library/style.css 22.9 kB +17 B (+0.07%)
build/styles/block-library/style.min.css 19.3 kB +18 B (+0.09%)

4de181b Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric 176d400 trunk % Change
timeToFirstByte 58.3 ms +8.32% -2.4% 56.3 ms +8.97% -1.95% 3.55%
largestContentfulPaint 98 ms +6.12% -6.12% 90 ms +13.33% -2.22% 8.89%
lcpMinusTtfb 36.75 ms +18.64% -7.35% 33.65 ms +14.26% -2.67% 9.21%
wpBeforeTemplate 28.61 ms +7.83% -0.66% 28.06 ms +7.48% -1.6% 1.96%
wpTemplate 25.33 ms +3.4% -4.03% 24.18 ms +5.79% -1.74% 4.76%
wpTotal 54.63 ms +7.74% -2.65% 52.66 ms +9.27% -2.03% 3.74%
wpMemoryUsage 7.67 MB +0% -0% 7.64 MB +0% -0% 0.45%
wpDbQueries 17 +0% -0% 17 +0% -0% 0%

front-end-classic-theme

Metric 176d400 trunk % Change
timeToFirstByte 48.2 ms +6.54% -0.93% 48 ms +7.4% -1.98% 0.42%
largestContentfulPaint 104 ms +7.69% -0% 104 ms +0% -1.92% 0%
lcpMinusTtfb 56.25 ms +2.31% -0.8% 54.8 ms +2.83% -2.92% 2.65%
wpBeforeTemplate 26.75 ms +2.62% -0.79% 26.57 ms +5.68% -2.18% 0.68%
wpTemplate 18.28 ms +7.22% -1.48% 17.77 ms +9% -1.58% 2.87%
wpTotal 45.15 ms +5.94% -1.09% 45.05 ms +6.9% -2.38% 0.22%
wpMemoryUsage 6.29 MB +0% -0% 6.25 MB +0% -0% 0.7%
wpDbQueries 14 +0% -0% 14 +0% -0% 0%

media-processing

Metric 176d400 trunk % Change
mediaProcessingJpeg 401.16 ms +2.5% -1.21% 400.38 ms +1.2% -1.05% 0.19%
mediaProcessingAvif 6048.57 ms +0.66% -0.2% 6054.13 ms +0.13% -0.13% -0.09%
mediaProcessingJpegToAvif 4176.16 ms +0.56% -0.33% 4181.23 ms +0.22% -0.23% -0.12%

media-upload

Metric 176d400 trunk % Change
jpegUploadProcessing 1437.62 ms +36.05% -1.71% 1418.04 ms +0.58% -0.92% 1.38%
pngUploadProcessing 186.53 ms +9.82% -7.88% 213.92 ms +6.9% -18.19% -12.8%
largeJpegUploadProcessing 1412.56 ms +0.81% -0.64% 1407.78 ms +0.5% -0.84% 0.34%
multipleImageUploadProcessing 1587.92 ms +1.55% -0.95% 1578.14 ms +2.5% -0.87% 0.62%

post-editor

Metric 176d400 trunk % Change
serverResponse 517.36 ms +4.46% -4.36% 483.18 ms +3.44% -5.71% 7.07%
firstPaint 267.43 ms +29.09% -9.98% 290.98 ms +49.1% -17.6% -8.09%
domContentLoaded 1127.04 ms +1.07% -1.66% 1110.65 ms +1.07% -0.51% 1.48%
loaded 1128.59 ms +1.08% -1.67% 1112.12 ms +1.05% -0.52% 1.48%
firstContentfulPaint 471.34 ms +2.24% -0.88% 462.25 ms +1.52% -1.93% 1.97%
firstBlock 3356.32 ms +0.69% -0.3% 3371.56 ms +1.76% -0.62% -0.45%
type 17.92 ms +2.73% -5.36% 18.36 ms +1.85% -1.2% -2.4%
typeWithoutInspector 17.73 ms +5.81% -1.92% 17.26 ms +2.61% -2.67% 2.72%
typeWithTopToolbar 24.48 ms +0.9% -3.35% 24.78 ms +5.41% -4.48% -1.21%
typeContainer 8.98 ms +6.79% -8.13% 9.12 ms +3.07% -2.85% -1.54%
focus 114.46 ms +6.85% -5.75% 110.14 ms +2.81% -3.26% 3.92%
firstFocus 238.09 ms +0% -0% 244.08 ms +0% -0% -2.45%
selectAll 557.65 ms +5.11% -2.08% 562.15 ms +4.43% -3.93% -0.8%
listViewOpen 78.86 ms +5.64% -2.92% 77.73 ms +5.35% -5.18% 1.45%
inserterOpen 41.32 ms +6.7% -5.83% 39.75 ms +2.84% -5.01% 3.95%
inserterHover 11.49 ms +11.05% -2.79% 11.98 ms +13.52% -7.85% -4.09%
inserterSearch 8.86 ms +12.08% -3.95% 8.47 ms +4.13% -5.55% 4.6%
loadPatterns 634.53 ms +3.08% -3.11% 654.62 ms +4.32% -4.74% -3.07%
wpTotal 507.04 ms +4.6% -4.38% 473.23 ms +3.42% -5.94% 7.14%
wpMemoryUsage 13.23 MB +0% -0% 13.20 MB +0% -0% 0.25%
wpDbQueries 54 +0% -1.85% 54 +0% -1.85% 0%

site-editor

Metric 176d400 trunk % Change
serverResponse 508.33 ms +1.74% -3.66% 507.76 ms +2.68% -5.98% 0.11%
firstPaint 305.08 ms +56.25% -14.18% 272.54 ms +64.37% -18.91% 11.94%
domContentLoaded 1205.5 ms +1.64% -3.46% 1186.91 ms +1.42% -3.53% 1.57%
loaded 1206.84 ms +1.62% -3.44% 1188.19 ms +1.43% -3.52% 1.57%
firstContentfulPaint 476.75 ms +1.41% -1.36% 471.03 ms +3.04% -2.62% 1.21%
firstBlock 4499.05 ms +1.9% -0.48% 4479.47 ms +0.44% -0.37% 0.44%
type 18.15 ms +6.28% -2.7% 18.63 ms +4.51% -3.81% -2.58%
navigate 115.99 ms +17.02% -2.6% 125.35 ms +7.67% -3.69% -7.47%
loadPatterns 1280.55 ms +5.05% -2.07% 1310.49 ms +5.81% -9.24% -2.28%
loadPages 1028.19 ms +1.15% -1.76% 999.25 ms +2.26% -0.98% 2.9%
wpTotal 498.61 ms +1.71% -3.65% 498.17 ms +2.65% -6.15% 0.09%
wpMemoryUsage 12.27 MB +0% -0% 12.23 MB +0% -0% 0.3%
wpDbQueries 43 +2.33% -0% 43 +2.33% -0% 0%

4de181b 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.

should update the URL from the last navigation if only varies in the URL fragment in /test/e2e/specs/interactivity/router-navigate.spec.ts, passed after 1 failed attempt.
Error: expect(locator).toHaveText(expected) failed

Locator:  getByTestId('title')
Expected: "Link 1"
Received: "Main"
Timeout:  5000ms

Call log:
  - Expect "toHaveText" getByTestId('title') with timeout 5000ms
  - waiting for getByTestId('title')
    14 × locator resolved to <h2 data-testid="title">Main</h2>
       - unexpected value "Main"

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/interactivity/router-navigate.spec.ts:160:25

4de181b Run

@jasmussen

Copy link
Copy Markdown
Contributor Author

Thanks a ton for the review. I rrebased on trunk, which pulled in #79058, which rebuilt the open animation, and it turned out to collide with this PR.

Fixing that properly meant moving the swipe off CSS classes altogether. The slide and fade are now element.animate() calls. As a result, the stylesheet side of this PR is down to a single line.

Your feedback should be addressed as part of this, even if the JS is now larger:

  • touchcancel is handled and the reset is collected in one place, so a cancelled drag no longer leaves the offset stuck across close and reopen.
  • direction starts as null.
  • is-fading-in is gone, along with the rest of the swipe CSS.
  • Changelog entry moved up to Unreleased.

On where the transform belongs: the containers rather than the img, and it moved off the overlay too. The overlay's style attribute is bound to state.overlayStyles, and a resize part way through a gesture rewrote it and wiped the drag out from under the finger, the containers have nothing bound to them.

It works pretty well for me:

state

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 [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.

4 participants