The canonical interaction rules. Every input method gets the behaviour its platform taught you.
Rules
| Input | Not zoomed | Zoomed |
|---|---|---|
| Touch swipe ← → | Next / previous, native scroll momentum | Pan (with inertia and rubber-band edges) |
| Touch swipe ↑ ↓ | Pull to dismiss | Pan |
| Pinch (touch, trackpad) | Zoom around the fingers; pinch below fit and release to close | Zoom; settles back into 1×–maxZoom |
| Double tap / double click | Zoom to the point (≥ 2×, or enough to fill the screen) | Back to fit |
| Tap image (touch) | Toggle data-ui-hidden |
Toggle data-ui-hidden |
| Tap / click backdrop | Close (closeOnBackdropClick) |
Close |
| Mouse drag ← → | Next / previous, with momentum (Blossom’s drag engine) | Pan |
| Mouse drag ↑ ↓ | Pull to dismiss | Pan |
| Trackpad two-finger swipe ← → | Native scroll-snap: the OS’s own physics | Pan |
| Wheel / two-finger scroll ↑ ↓ | — (or zoom with wheelToZoom) |
Pan (or zoom with wheelToZoom) |
| Ctrl / ⌘ + wheel | Zoom around the cursor | Zoom around the cursor |
| ← → | Previous / next | Previous / next |
| + − 0 | Zoom in / out / reset | Zoom in / out / reset |
| Esc | Close | Close |
Why it feels native
- Swiping is real scrolling. The viewport is a CSS
scroll-snapcontainer, so touch swipes get the OS’s own momentum and overscroll. Idle slides usetouch-action: pan-x, which hands horizontal drags to the browser and gives us vertical ones for free: direction locking without heuristics. - Trackpads are left alone. A two-finger swipe is plain native scrolling with CSS scroll-snap — no JavaScript wheel handling at all, so it feels exactly like every other scroller on your Mac. Mouse dragging is added on top by Blossom Carousel, which only loads on devices with a mouse (0kb on touch).
- Three-finger drag pauses on purpose. If you drag with three fingers (Accessibility → Pointer Control → Trackpad Options), macOS holds the drag for a moment after you lift so you can reposition. Every app sees that delay; use a two-finger swipe to page.
- Everything is interruptible. Grab a photo mid-flight and it stops under your finger. Close while it’s still opening and it reverses from wherever it is. Every animation starts from the image’s current visual state.
- Velocity carries over. A flick to dismiss feeds its speed into the spring that flies the photo home. Panning ends in inertia that bounces off the edges.
- Rubber bands, not walls. Over-zooming and over-panning resist progressively (the iOS curve), then settle.
- The page keeps up. As you navigate, the page behind the viewer scrolls the current thumbnail into view
(
scrollTriggerIntoView). When you close, the photo always has somewhere visible to land; if it doesn’t, it fades. - Reduced motion is respected. With
prefers-reduced-motion, morphs become short fades.