Three ways to bring Shoji in, all produced by the same npm run build:
Core plus every official plugin, in one JS file and one CSS file. Nothing activates until you
opt a gallery into it via plugins: [...], so unused plugins cost nothing at
runtime beyond their (small) parse/download size.
<link rel="stylesheet" href="shoji.min.css" />
<script src="shoji.min.js"></script>
<script>
new Shoji('#gallery');
</script>
The UMD build exposes exactly one global: Shoji. It's the constructor itself (not
a namespace object), and official plugins hang off it as static properties —
Shoji.Zoom, Shoji.Layout, etc.
Same zero-build-step story as the single file above, but split apart — for when you want, say, Zoom and Autoplay but not the other five official plugins, without adopting a bundler just to trim them.
<link rel="stylesheet" href="core/shoji-core.min.css" />
<link rel="stylesheet" href="plugins/zoom.min.css" />
<script src="core/shoji-core.min.js"></script>
<script src="plugins/zoom.min.js"></script>
<script>
new Shoji('#gallery', { plugins: [Shoji.Zoom] });
</script>
core/shoji-core.min.js must load before any plugins/*.min.js file —
each plugin script attaches itself onto the Shoji global the core script just
created (Shoji.Zoom, same as the combined bundle above), not the other way
around. Plugin script tags themselves can be in any order relative to each other, and CSS
order doesn't matter at all. Some plugins (RotateFlip, Fullscreen,
Video) have no CSS of their own — no <link> tag needed for
those. ActiveThumbnail ships CSS only for its opt-in
highlight option (see the Plugins guide) — the <link> tag is
only needed if you use it.
npm install @michaelyagi/shoji@beta
import Shoji from '@michaelyagi/shoji';
import '@michaelyagi/shoji/style.css';
new Shoji('#gallery', { plugins: [Shoji.Zoom] });
Bundler users who want to tree-shake away plugins they don't use can import the class and plugins separately instead of pulling in the default export's static properties:
import { Gallery } from '@michaelyagi/shoji/core';
import { Zoom } from '@michaelyagi/shoji/plugins/zoom';
new Gallery('#gallery', { plugins: [Zoom] });
Every option in Shoji has a default chosen so this is a complete integration — no items array, no options object, no plugins:
<div id="gallery">
<a href="photo-1-full.jpg"><img src="photo-1-thumb.jpg" alt="Sunset over the bay"></a>
<a href="photo-2-full.jpg"><img src="photo-2-thumb.jpg" alt="Mountain trail"></a>
</div>
new Shoji('#gallery');
This works because of a default DOM→item mapping in selector mode:
| DOM | Item field |
|---|---|
<a href> (or data-shoji-src) |
src |
nested <img src> (or data-shoji-thumb) |
thumb |
that <img>'s alt (or data-shoji-alt) |
alt |
data-shoji-caption |
caption |
data-shoji-width / data-shoji-height |
width / height |
data-shoji-id |
id (falls back to src) |
any other data-shoji-* attribute |
data['<name>'] — e.g. data-shoji-metadata-id="123" →
item.data['metadata-id']
|
data-shoji-width/data-shoji-height are optional, but worth supplying
whenever you know the full image's real pixel size: without them, Shoji no longer guesses at a
size for the open animation or the open-transition placeholder — both are skipped entirely
(plain spinner instead) rather than risk getting it visibly wrong. Supplying them is what gets
you the "grows out of the thumbnail you clicked" open animation at all — see
Transitions.
Any data-shoji-* attribute that isn't one of the named mappings above lands in
item.data instead, keyed by the attribute name with the
data-shoji- prefix stripped verbatim (not camelCased) — the same free-form bucket
dynamic-mode items already use for host payloads, just populated from the DOM here:
<a href="photo-1-full.jpg" data-shoji-metadata-id="123">
<img src="photo-1-thumb.jpg">
</a>
const MyPlugin = {
name: 'myPlugin',
init(ctx) {
ctx.on('afterOpen', ({ index }) => {
console.log(ctx.gallery.items[index].data); // { 'metadata-id': '123' }
});
},
};
item.data stays undefined when an item has no custom
data-shoji-* attributes — this never adds an empty object. In dynamic mode
there's no DOM to scan, so item.data is only ever whatever you put there yourself
when constructing the item.
Each matched element also gets a click listener that opens the gallery at its index and calls
preventDefault(), so a real <a href> never navigates away —
the one piece of actual behavior (not just a default) the quickstart needs to work at all.
A nested <video> is auto-detected too — plain HTML5 video markup just
works, no extra config:
<a href="clip.mp4">
<video poster="clip-poster.jpg">
<source src="clip.mp4" type="video/mp4">
</video>
</a>
A data-shoji-video URL pointing at YouTube or Vimeo is recognized too —
<a data-shoji-video="https://youtu.be/dQw4w9WgXcQ"> becomes a
{ provider: 'youtube', id } item automatically, and likewise
<a data-shoji-video="https://vimeo.com/76979871"> becomes
{ provider: 'vimeo', id }; anything else falls back to a plain HTML5 source.
Rendering either needs the
Video plugin (Shoji.Video) — core only detects the
provider, it doesn't embed one.
YouTube detection covers youtu.be/<id>, watch?v=,
/embed/, /shorts/, /live/ (streams/premieres),
/v/ (legacy), and youtube-nocookie.com. Vimeo detection covers
vimeo.com/<id> (including an unlisted video's
/<id>/<hash> privacy hash) and
player.vimeo.com/video/<id>. Both are still just pattern matching against
known URL shapes, not a guarantee. An optional data-shoji-video-id attribute
overrides the id that would otherwise be parsed out of the URL — useful either to rescue a URL
shape detection doesn't recognize, or to skip parsing entirely:
<a data-shoji-video="https://youtu.be/dQw4w9WgXcQ" data-shoji-video-id="dQw4w9WgXcQ">
The link (data-shoji-video) is still required either way — the id attribute only
ever overrides the id, never the URL itself. If it's set but
data-shoji-video clearly isn't a recognized YouTube or Vimeo host, it's ignored
(falls back to html5) with a console warning — same if it's set without any
data-shoji-video at all, or on a real <video> element (always
html5, regardless).
A third attribute, data-shoji-video-provider, declares the provider outright —
the DOM-mode equivalent of dynamic mode's explicit video.provider. For
YouTube/Vimeo it just makes the claim explicit (still trusting the id even off a host neither
recognizes, e.g. an internal proxy link); wistia isn't a supported provider at
all — an unrecognized data-shoji-video-provider value is ignored with a warning
and falls back to normal auto-detection:
<a
data-shoji-video="https://youtu.be/dQw4w9WgXcQ"
data-shoji-video-provider="youtube"
data-shoji-video-id="dQw4w9WgXcQ"
>
Whichever mode builds them (selector-mode scanning, above, or your own objects in dynamic
mode, below), every item is the same shape. src is the only required field —
everything else is optional, read where it's relevant, and simply skipped if absent:
| Field | Required? | Used for |
|---|---|---|
src |
yes |
The lightbox slide's image (or video file, for a plain HTML5 video with no
sources).
|
thumb |
— |
The grid/tile image — Layout plugin tiles prefer this over src (see the
Layout guide). Also the preferred source for the
open-transition placeholder — see
Transitions.
|
id |
— |
Two separate uses, with different fallback behavior. For
updateSlides diffing, it falls back to src if omitted. For
finding the origin thumbnail — used by the open/close zoom transition and
ActiveThumbnail, via a data-shoji-id element matching this
id — there is no fallback: omit id, or omit
the matching data-shoji-id attribute on your thumbnail element, and the
origin lookup returns nothing. No error is thrown; the zoom transition and
ActiveThumbnail just silently don't do anything. See
Transitions for what's required to
enable it.
|
width / height |
— | Aspect-ratio placeholder before the image loads, measure-free Layout plugin positioning, and — required, not just preferred — for the open animation and open-transition placeholder to show at all (both are skipped, plain spinner instead, without this) — see Transitions. Should be the original's dimensions, not the thumbnail's. |
alt |
— |
The rendered image's alt text and the screen-reader slide announcement.
|
caption |
— |
Rendered below the active slide.
string | HTMLElement | { dangerouslySetInnerHTML }.
|
srcset / sizes |
— |
Passed straight through to the slide's <img> for responsive/DPR-aware
source selection.
|
video / sources |
— |
Makes it a video slide instead of an image; sources is one entry per
<source src type>. In dynamic mode only, video also
accepts the shorthand true — infers the whole descriptor from
src, the dynamic-mode equivalent of a bare
data-shoji-video="<url>" with no explicit id. A youtube descriptor's
own id is likewise optional in dynamic mode: omit it and it's filled in
from src the same way. Either way, a real id you do supply
always wins and is never touched; if none can be filled in, a console warning explains
why and the slide shows a placeholder instead of a broken embed.
|
poster |
— |
Video poster frame; also what Layout tiles fall back to (after thumb) for a
video item.
|
data |
— |
Free-form payload for your own plugins/handlers to read — never touched by core
rendering. In selector mode, also auto-populated from any unrecognized
data-shoji-* attribute.
|
download |
— | Declared on the type; not yet wired to any UI by core or a shipped plugin. |
No DOM markup at all — pass an items array and call open(index)
yourself (there's no default click target for the gallery to attach a listener to):
const gallery = new Shoji('#gallery', {
items: [
{ id: 'p1', src: 'photo-1-full.jpg', thumb: 'photo-1-thumb.jpg', alt: 'Sunset over the bay' },
{ id: 'p2', src: 'photo-2-full.jpg', thumb: 'photo-2-thumb.jpg', alt: 'Mountain trail' },
],
});
document.querySelector('#my-thumb-1').addEventListener('click', () => gallery.open(0));
Update the item list at any time with
gallery.updateSlides(items, currentIndex?) — items are diffed by
id (falling back to src), preserving the active slide when possible.
addSlides(items, atIndex?) and removeSlides(match) are sugar over
the same call for the two most common cases.
This page covers just enough to get a gallery on screen. The rest of the API has its own dedicated guides, not duplicated here: