Zoom & a11y

How ramka’s zoom & pan — on desktop and on touch — lines up with mainstream image viewers, and how it maps to WCAG 2.2. For the wiring and gesture ownership, see Gestures.

Desktop conventions

Desktop image viewers split into two families, depending on whether the surface is treated as a media viewer or a canvas / document:

  • Media-viewer model (Google Photos, Lightroom, PhotoSwipe, OpenSeadragon): a bare scroll wheel zooms toward the cursor; drag pans.
  • Canvas / document model (Figma, macOS Preview, Google Maps on trackpad): a bare scroll pans, and zoom is gated behind a modifier (Ctrl / ⌘) or a trackpad pinch — so it never hijacks the user’s scroll.

Regardless of family, four things are near-universal: drag pans when zoomed, wheel/pinch zoom is focal (zooms toward the pointer), double-click toggles zoom, and Ctrl/⌘+wheel and trackpad pinch always zoom. The only real disagreement is whether a bare wheel zooms or pans.

AppBare scrollCtrl/⌘+wheel · pinchDouble-clickDrag
macOS PreviewPan / scrollZoom (pinch)Smart-zoom togglePan
Apple Photos (Mac)Zoom (pinch)Toggle 1×↔zoomPan
Windows PhotosNavigateZoom (Ctrl+wheel)TogglePan
Google Photos (web)Zoom to cursorZoomTogglePan
Google MapsZoom to cursorZoomZoom-in stepPan
PhotoSwipe v5Opt-in (wheelToZoom)Zoom (pinch)TogglePan
OpenSeadragonZoom to cursorZoomZoom-in stepPan
LightroomZoomZoomToggle 1:1Pan
Figma / Miro (canvas)PanZoom to cursorPan (space)

ramka on desktop

ramka defaults to the canvas model: Ctrl/⌘+wheel and trackpad pinch zoom to the cursor, a bare wheel is a no-op at 1× (and pans when zoomed), and scrollToZoom on Zoom opts into the media-viewer behavior. That is the more embed-friendly default — a lightbox often lives inside a scrollable page, so bare-wheel zoom risks trapping the user’s scroll.

InteractionramkaConsensusVerdict
Trackpad pinchZooms to cursor (Safari GestureEvent; Chrome/FF via ctrlKey+wheel); elastic past bounds, snaps backZoom to cursorMatch, cross-browser
Ctrl / ⌘ + wheelZooms, focal at cursor; hard-clamped at min/max (not a rubber-band pinch)Zoom to cursorMatch
Bare scroll wheelOff by default (scrollToZoom=false): no-op at 1×, pans when zoomedSplit — media viewers zoom, canvas apps panDefensible default; opt-in covers the other
Double-clickToggles zoom, focal at cursorToggle, focalMatch
Drag to panYes — grab / grabbing cursor, elastic + momentumUniversalMatch
Wheel to pan (zoomed)Yes (deltaX / deltaY)CommonMatch
Zoom buttonsZoomIn / ZoomOut, step ×2CommonZooms about center, not cursor
Keyboard zoom+ = − _ and ⌘/Ctrl+0ExpectedMatch
Keyboard panArrows pan (50px) when zoomedExpectedMatch
Max zoomAuto-capped to image's native resolution (fallback 4×)Best practice (avoids blur)Better than many
Cursor at 1×No zoom-in affordance (consumer CSS)Some apps show a zoom-in cursorMinor discoverability gap

The one intentional inconsistency: ZoomIn / ZoomOut zoom about the center rather than the cursor. That is standard — buttons have no pointer position — and matches Google Maps / OpenSeadragon button behavior.

Mobile conventions

Touch is far more uniform than desktop — every mainstream photo viewer converges on the same model, set by iOS Photos and Android’s Google Photos:

  • Pinch to zoom, focal at the pinch midpoint — the image stays glued to the centroid, so spreading and sliding two fingers zoom and pan as one gesture — with a rubber-band overshoot past the min/max that snaps back on release.
  • Double-tap toggles between fit and a zoomed level, centered on the tap point.
  • One- or two-finger drag pans while zoomed, with inertia / momentum on release.
  • At fit, a horizontal swipe navigates and a downward swipe dismisses the viewer.

The subtle-but-critical part is arbitration: pinch, pan, swipe-to-navigate, and swipe-to-dismiss all start from the same finger, so a viewer has to decide which gesture owns a given touch without misfiring.

AppPinchDouble-tapPanMomentumSwipe down
iOS PhotosFocal, rubber-bandToggle at tap1-finger dragYes + overscrollDismiss
Android Google PhotosFocal, rubber-bandToggle at tap1-finger dragYesDismiss
InstagramFocal (peek), snaps backLike (not zoom)While pinchedNoDismiss
Mobile Safari (page)FocalToggle (smart)1-finger dragYes
PhotoSwipe (mobile)Focal, rubber-bandToggle at tap1-finger dragYesDismiss
Telegram / WhatsAppFocalToggle at tap1-finger dragYesDismiss

ramka on mobile

ramka implements the full iOS/Android touch model, including the arbitration details that make it feel native — deferred double-tap commit (so a flick that starts near the last tap never zooms by mistake), elastic overscroll on both pinch and pan, momentum with bound-clamping, and a haptic tick on snap-back. Pull-to-dismiss and zoom/pan share a navigation lock so an in-flight Slides fling can’t be hijacked mid-scroll.

InteractionramkaConsensusVerdict
Pinch to zoomFocal at pinch midpoint; image follows the centroid (pinch + pan together); elastic past bounds, snaps back with haptic tickFocal + rubber-bandMatch
Double-tapToggles zoom, focal at tap; commit deferred to pointerup so a flick never misfiresToggle at tapMatch (robust detection)
1-finger pan (zoomed)Elastic drag; velocity trackedUniversalMatch
2-finger pan (zoomed)Centroid pans immediately; zoom stays frozen until fingers actually pinch (iOS-style recognizer slop), then scale rebases so jitter never popsiOS Photos / pinch-panMatch
Release with velocityMomentum pan (decelerates, clamps to bounds)iOS/Android inertiaMatch
Swipe left / rightNavigates via Slides snap carousel (only at 1×)Navigate photosMatch
Swipe downPull-to-dismiss; blocked while zoomed > 1×DismissMatch
touch-actionnone when zoomed; pan-x pan-y at 1× (lets Slides + page own the swipe)Guard native gesturesCorrect
Native page pinch-zoomSuppressed while Content is open so it never fights Lightbox.ZoomPrevent conflictMatch
HapticsLight tick on snap-backiOS rubber-band feelBetter than many

The touch model is where ramka is strongest — it matches the platform viewers point-for-point and adds haptics that most web lightboxes skip. Note that Instagram-style pinch-to-peek (zoom only holds while fingers are down, then springs back to fit) is a deliberate product choice, not the default; ramka keeps the zoom level after release like Photos, which is the right behavior for a general-purpose viewer.

UX heuristics

  • Zoom to the cursor. The single most important pointer-zoom heuristic. ramka honors it for wheel, pinch, and double-click.
  • Don’t hijack bare scroll by default. Keeping scrollToZoom off is the safer default for embedded lightboxes; enable it for full-bleed, single-image viewers.
  • Signal zoomability at rest. Consider a zoom-in cursor over the image at 1× so mouse-only users discover double-click / Ctrl-wheel zoom. ramka leaves this to your CSS — the Zoom surface exposes data-zoom only while scaled past 1×. Set cursor: zoom-in on your Zoom class — ramka’s inline cursor (grab / grabbing) wins once scaled:
/* .my-zoom is the className you pass to Lightbox.Zoom */
.my-zoom {
  cursor: zoom-in;
}
  • Never steal swipe-to-dismiss. On touch, pan must only take over after the image is zoomed; at fit, the vertical swipe belongs to pull-to-dismiss and the horizontal swipe to navigation. ramka gates this with touch-action and the navigation lock.
  • Rubber-band, don’t hard-stop. Elastic overscroll on pinch and pan (plus a haptic tick on snap-back) is what makes zoom feel native rather than mechanical.
  • Size touch controls for fingers. Give ZoomIn / ZoomOut and Close a hit area of at least 44×44 px on touch (WCAG 2.5.8 asks ≥24 px; 44 px is the platform norm).

WCAG conformance

These are the WCAG 2.2 success criteria that govern zoom & pan. ramka ships the pointer and keyboard alternatives the criteria expect:

Success criterionLevelApplies toramka
2.5.1 Pointer GesturesAPinch (touch + trackpad) is a multipoint gesture — needs a single-pointer alternative.Pass. Double-tap / double-click and the ZoomIn / ZoomOut buttons are all single-pointer.
2.5.7 Dragging MovementsAAPan is drag-based and ramka implements its own pan (preventDefaults wheel), so the author-content clause applies.Partial. Keyboard arrows + wheel-pan exist, but a strict single-pointer, non-drag pan alternative (e.g. pan controls) is not built in. See note below.
2.1.1 KeyboardAAll zoom/pan must be reachable by keyboard.Pass. +/−/0 and arrows route through the active-zoom bridge in Content.
2.5.8 Target Size (Minimum)AAZoom / close controls should be ≥24×24 CSS px (44 px is the touch norm).Consumer. Sizing is your CSS in a headless library — a guidance item, not a library defect.

Note on 2.5.7. Pan-by-drag has a keyboard alternative (arrows), which satisfies 2.1.1 Keyboard, but 2.5.7 specifically wants a single-pointer, non-drag alternative — keyboard does not count for it. If you must meet AA strictly, add pointer pan controls (e.g. edge/arrow buttons) alongside ZoomIn / ZoomOut. Many shipping viewers do not either, but for enterprise targets it is worth flagging.