Everything beyond the minimal lightbox is a plugin, including features you might expect to be
built in (zoom, fullscreen, rotate/flip). Core stays small; plugins are opt-in, one at a time,
via the plugins array.
new Shoji('#gallery', {
plugins: [Shoji.Zoom, Shoji.Fullscreen],
});
Every official plugin's defaults are chosen so that just appearing in plugins
is a complete, working config — no per-plugin options object required. Add one only to
override something:
new Shoji('#gallery', {
plugins: [Shoji.Zoom],
zoom: { maxScale: 8 },
});
options[plugin.name] convention
Each plugin's options nest under a top-level key matching its name —
zoom: {...} for the plugin named 'zoom',
layout: {...} for 'layout', and so on. This applies the same way to
custom plugins you write yourself.
| Plugin | Static / option key | What it does | Key options (defaults) |
|---|---|---|---|
| Zoom | Shoji.Zoom / zoom |
Pinch, double-tap/double-click, wheel/trackpad-scroll, and three toolbar buttons (zoom in/out, actual size) — pan by dragging once zoomed. Buttons are hidden entirely on a video slide (nothing to zoom). |
maxScale (4), doubleTapScale (2),
buttonStep (1.5), mouseWheelZoom (true — any
wheel event zooms, the only way to also catch a trackpad's plain two-finger drag;
'ctrl' restricts it to ctrl+wheel, false disables it)
|
| Fullscreen | Shoji.Fullscreen / fullscreen |
Toolbar toggle that puts the whole lightbox (not just the media) into the browser's native fullscreen mode. No button rendered at all on browsers without Fullscreen API support. | none — no options object |
| RotateFlip | Shoji.RotateFlip / rotateFlip |
Four toolbar buttons (rotate left/right, flip horizontal/vertical). Non-destructive CSS transform, resets every slide change. For persistent, pixel-level edits you'd need a different plugin — Shoji doesn't ship one. | none — no options object |
| Autoplay | Shoji.Autoplay / autoplay |
Toolbar play/pause + Space. Video slides play through to their own
ended event instead of using the fixed interval; a manual pause on a video
pauses the whole slideshow. Pauses by default while the viewer is actively zoomed in,
interacting with rotate/flip, or reading an expanded caption — 'stop' stays
paused until Play is pressed again, 'pause' resumes on its own once
genuinely disengaged, false ignores it entirely.
|
interval (5000ms), showProgress (true),
autoStart (false), onZoom ('stop'),
onRotateFlip ('stop'),
onCaptionExpand ('stop'),
stopOnManualNavigate (true)
|
| ActiveThumbnail | Shoji.ActiveThumbnail / activeThumbnail |
Keeps a host's own thumbnail grid in sync with whichever slide is open — toggles a
class, scrolls it into view, and (unlike a plain "clear on close") leaves the class in
place after the lightbox closes: the point of a visible marker is seeing it once the
backdrop is out of the way, so only a genuinely different slide becoming active (or
destroy()) moves or removes it. No default styling shipped for
activeClass itself — write your own CSS:
(a negative offset, not positive — pulling the outline inward keeps it from
spilling into a neighboring tile in a tightly-gutter'd grid, the same real bug
highlight's own built-in styling below hit and fixed) — or opt into
highlight: true instead of writing any CSS at all. Live in the
Autoplay + ActiveThumbnail example. Also needs a data-shoji-id match per item outside selector mode — see
Getting started's GalleryItem fields
table.
|
activeClass ('shoji-thumb-active'),
scrollIntoView (true), highlight (false — opt-in outline
styling), borderColor ('blue', only visible with
highlight: true), highlightDuration (ms, unset — opt-in
auto-fade of the highlight styling, counted from close()),
highlightFadeDuration (ms, 800 — how long that fade itself takes, only
meaningful once highlightDuration is set)
|
| Layout | Shoji.Layout / layout |
Turns the inline gallery container into a managed grid/masonry/justified layout. | See the dedicated guide. |
| Video | Shoji.Video / video |
Renders { video: { provider: 'youtube' | 'vimeo', id } } items as a real
embed — core's own native-<video> rendering only handles
provider: 'html5'. Shows a poster while the embed loads:
item.poster if set, else a fallback thumbnail (YouTube's predictable
hqdefault.jpg URL; Vimeo's own oEmbed-fetched one), else the plain spinner.
Autoplay's slideshow waits for the real player's own ended
state, same as native video.
|
none — no options object |
A plugin is a plain object — no base class. The full shape:
const MyPlugin = {
name: 'myPlugin',
version: '1.0.0', // optional — yours to track; core never reads it
requires: [], // optional — other plugin names that must also be in `plugins`
defaults: { message: 'Hello from a plugin' },
init(ctx) {
const message = String(ctx.options.message);
const unsubscribe = ctx.ui.toolbar('right', {
label: message,
onClick: () => alert(message),
});
return () => unsubscribe(); // cleanup, run on destroy()/reinit()
},
};
new Shoji('#gallery', { plugins: [MyPlugin], myPlugin: { message: 'Hi!' } });
| Field | Required | What it does |
|---|---|---|
name |
Yes |
Unique identifier. The key options[name] is read from, what
requires/getActivePlugins() refer to it by, and what shows up
in the "unmet dependency" error if something else's requires names it and
it's missing.
|
version |
No | A string you set for your own tracking/debugging. Purely informational — nothing in core reads it today. |
requires |
No |
string[] — the required plugins' own name strings,
e.g. ['zoom'], never the plugin objects themselves (not
[Shoji.Zoom]). Checked against the whole declared
plugins array — position doesn't matter, a dependency can be listed before
or after the plugin that needs it. See the callout just below.
|
defaults |
No |
This plugin's own default options — merged underneath whatever the host passed as
options[name] before ctx.options is built. Omit it entirely if
your plugin takes no options.
|
init(ctx) |
Yes |
Runs once, synchronously, at new Shoji() construction time (not lazily on
first open()). Return nothing, or return a cleanup function — called on
destroy()/reinit() for anything you set up by hand outside
ctx (everything registered through ctx is already
disposed automatically, see below).
|
The one easy way to getrequireswrong — it fails silently (logged to the console, not thrown), which is exactly what makes it easy to miss: the dependent plugin'sinit()just never runs, so whatever it registers (a toolbar button, in this example) simply never appears, with no error visible unless you're looking at the console.// Wrong — plugin objects, not their name strings: requires: [Shoji.Zoom, MyOtherPlugin] // Right — name strings. Position in `plugins` doesn't matter either way — // MyOtherPlugin can be listed before *or* after ThisPlugin, both work: requires: ['zoom', 'myOtherPlugin']requiresis checked against the whole declaredpluginsarray, not registration order — a dependency only needs to be somewhere in the list, not listed earlier than whatever needs it. It even resolves chains correctly: ifmyOtherPluginitself had an unmetrequires,ThisPluginwould be skipped too, not just left silently assuming a broken dependency was fine. What never changes regardless: every plugin'sinit()still runs — and everyctx.ui.toolbar()button still lands — in exactly the order thepluginsarray itself declares.
requires at all?
requires doesn't hand you a reference to the other plugin, and it doesn't wire
anything up — a plugin can call ctx.on(...) for an event another plugin emits
whether or not it's listed in requires, and it works exactly the same either way
once that other plugin is actually loaded. What requires buys you is a fail-fast
guard for the case where it isn't loaded: instead of your plugin quietly initializing
anyway and sitting there doing nothing (a subscription that never fires, a button that never
updates — no error, no obvious symptom), it gets skipped up front with a clear console error
naming exactly what's missing.
For example, a toolbar readout that shows the current zoom level only makes sense once
Shoji.Zoom is also loaded —
it works purely by listening for Zoom's own zoomChange event:
const ZoomIndicator = {
name: 'zoomIndicator',
requires: ['zoom'], // reads Zoom's own zoomChange event — pointless without it
init(ctx) {
const unsubscribe = ctx.ui.toolbar('left', { label: '100%', onClick: () => {} });
const off = ctx.on('zoomChange', ({ scale }) => {
// update the toolbar label to `${Math.round(scale * 100)}%`
});
return () => {
unsubscribe();
off();
};
},
};
Leave out requires: ['zoom'] and the code above still behaves identically
whenever Shoji.Zoom is in plugins. The difference only
shows up when it isn't: without requires, ZoomIndicator still
initializes, its button still renders, and it just sits stuck at 100% forever —
zoomChange never fires, with nothing telling you why. With
requires: ['zoom'], that same misconfiguration gets skipped entirely, logged as
an unmet dependency instead of a silently dead button.
What ctx gives you:
ctx.gallery — the full public Gallery API (navigation, events,
updateSlides, etc.).
ctx.options — this plugin's own merged options (defaults +
whatever the host passed under options[name]).
ctx.on(event, fn) / ctx.emit(event, detail) — the same typed event
bus gallery.on() reads from. Works in both directions: listen for another
plugin's state (zoomChange above), or command one to act —
ctx.emit('requestZoomIn', {}) zooms in without importing Zoom at all. See the
Events guide's "Plugin commands" table for the
full list.
ctx.ui.toolbar(slot, el) — add a button/element to the 'left',
'center', or 'right' toolbar slot. 'right' is where
every official plugin puts its controls (inserted immediately before the close button). If
too many buttons end up sharing a row for the viewport to fit, core collapses the extra ones
(latest-registered first) into a small popover behind a caret automatically — nothing a
plugin needs to handle itself. How many stay pinned before that kicks in is the host's own
maxPinnedToolbarButtons setting (default 2) — see the
Settings guide.
ctx.ui.overlay(el, layer?) — mount an element into the dialog, absolutely
positioned by you.
ctx.ui.outer() — the whole lightbox element (backdrop + dialog + controls), for
something like Fullscreen that needs to act on the entire lightbox.
ctx.ui.registerShortcut(key, fn) — a keyboard shortcut, scoped to while the
lightbox is open.
ctx.ui.registerVideoProvider(name, renderFn) — supplies the embed for a
non-'html5' video.provider value (e.g. 'youtube').
renderFn(container, item, onReady, signal) builds into
container and calls onReady() once it should show;
signal fires 'abort' once the slide is no longer wanted — release
everything there, since setup is often async and may resolve after eviction.
ctx.storage — per-gallery scratch storage, cleared on destroy().
Rules worth knowing: plugins talk to each other only through events, never direct imports —
requires: ['otherPlugin'] declares a hard dependency (checked against the whole
declared plugins array, not registration order; a missing or failed dependency
logs an error and skips that plugin, without taking down the rest). Everything registered
through ctx is disposed automatically on destroy()/reinit()
— the function your init() returns is for anything else you set up by hand.
Plugins init eagerly, at construction time — not lazily on first open() — so
ctx.ui.* always has real DOM to act on synchronously inside init().
gallery.getActivePlugins() returns the names of plugins that actually initialized
on that instance — string[], deduplicated, excluding an invalid
plugins[] entry (e.g. a stale dist build referencing a plugin static that doesn't
exist) or one skipped for an unmet requires. There's no separate "official vs.
custom" distinction anywhere in the API — an official plugin like Shoji.Zoom and
one you write yourself go through the exact same plugins array and the exact same
loading path, so this list can't and doesn't tell them apart either.
new Shoji('#gallery', { plugins: [Shoji.Zoom, Shoji.RotateFlip] });
gallery.getActivePlugins(); // ['zoom', 'rotateFlip']