Shoji

Events

Every feature is observable: gallery.on(event, fn) returns an unsubscribe function.

const off = gallery.on('afterSlide', ({ from, to }) => console.log(from, '→', to));
off(); // stop listening

None of these are cancelable right now — fn just receives a plain detail object, not a real Event; calling .preventDefault() on it would throw. The before*/after* naming is a lifecycle-order convention, not an implemented cancel mechanism — that's a documented future intent, not current behavior, so don't rely on being able to stop something from a before* handler today.

Core lifecycle

Event Detail Fires
beforeOpen { index } Right at the start of open(), before any DOM change.
open { index } After the lightbox DOM is built/shown, before the zoom-in transition starts.
afterOpen { index } Same tick as open currently — kept distinct for future use.
beforeClose {} First thing inside close(), before anything (including zoom/rotate plugins resetting their own state) reacts.
close / afterClose {} Once the zoom-out-to-thumbnail animation finishes (or immediately, under prefers-reduced-motion or with no origin thumbnail to animate to).
destroy {} End of destroy(), after all teardown — the last event this bus ever emits.
captionModalChange { open } The truncated-caption "read the rest" modal opening/closing — unrelated to the gallery's own open/close above, despite the similar shape. Autoplay's onCaptionExpand (Settings) reacts to open: true.

Slides & items

Event Detail Fires
beforeSlide / afterSlide { from, to } Around every navigation — buttons, keyboard, goTo(), a completed swipe.
slideItemLoad { index } Once the active slide's media has actually settled (image decoded, or video's loadedmetadata) — including instantly, if it was already preloaded. Never fires for a background/preloading neighbor that isn't active yet.
itemsUpdated { items } After updateSlides()/addSlides()/removeSlides()/refresh() change the item list.

Auto-hide

Event Detail Fires
controls:hide / controls:show {} Every auto-hide transition — sync points for a plugin with its own overlay that should fade along with the built-in controls. See gallery.controlsHidden on the Methods guide for the current-state equivalent.

Gesture relay

Core drives horizontal drag-to-navigate and vertical drag-to-close itself — no event for either. These have no built-in effect at all; they exist purely so a plugin (the Zoom plugin consumes all of them) can react without core depending on that plugin existing. Coordinates are viewport (client) pixels.

Event Detail
tap { x, y, controlsWereHidden } — controlsWereHidden is a snapshot from before this same tap's own pointerdown-triggered controls reveal ran (a live read of gallery.controlsHidden at tap time would always say false, since that reveal already happened by then). Useful for a plugin that wants to tell "this tap was just revealing hidden controls" apart from "this tap landed on an already-visible gallery."
doubleTap { x, y }
pinchStart { centerX, centerY }
pinchMove { scale, centerX, centerY } — scale is relative to this pinch's own start distance (1 = unchanged), not cumulative across separate pinches.
pinchEnd {}
wheelZoom { deltaScale, x, y } — deltaScale is a small per-event increment (positive = zoom in); a consumer accumulates it, it isn't an absolute scale.

Plugin-emitted

Event Detail From
autoplayStart / autoplayStop {} Autoplay
fullscreenChange { fullscreen } Fullscreen — from the native fullscreenchange event, not the toolbar button's click, so it reflects reality even when fullscreen was entered/exited by other means (browser Escape, a request rejected).
rotateFlipChange { index, flipH, flipV, rotation } RotateFlip, on every click — the plugin never persists this itself (resets per slide); store the payload yourself if you need to keep it.
zoomChange { index, scale } Zoom, on every scale change (gesture or button-driven) — same non-persisted pattern.
layoutRender { tiles: { index, element }[] } Layout, after a render pass builds/appends tile DOM — tiles is exactly what was built this pass, for injecting custom content (a badge, a selection checkbox) without re-deriving which tiles are new. See the Layout guide.

Plugin commands

The opposite direction from everything above: instead of listening for a plugin's state change, a custom plugin's own button can command one of these four to act — ctx.emit('requestZoomIn', {}) — without importing the target plugin at all. Each one mirrors its real toolbar button exactly: same function, same edge-case handling. A no-op if the target plugin isn't loaded.

Command Effect Handled by
requestAutoplayStart Starts the slideshow — a no-op while already playing. Autoplay
requestAutoplayStop Stops the slideshow — a no-op while already stopped. Autoplay
requestZoomIn / requestZoomOut Same fixed step as the toolbar buttons/w/s shortcuts. Zoom
requestZoomActualSize Toggles between the image's native resolution and neutral, same as the button. Zoom
requestZoomReset Returns straight to neutral from any zoom level — no toolbar-button equivalent of its own (requestZoomActualSize only resets conditionally). Zoom
requestRotateLeft / requestRotateRight Same 90° step as the toolbar buttons. RotateFlip
requestFlipHorizontal / requestFlipVertical Same toggle as the toolbar buttons. RotateFlip
requestFullscreenToggle Enters or exits fullscreen, same as the toolbar button. Fullscreen