Shoji

Theming

No hardcoded colors or sizes anywhere in Shoji's CSS or JS — everything routes through --shoji-* custom properties. Dark is the default theme; add the shoji-light class to switch to light.

Important: the lightbox (.shoji-outer) is always appended directly to <body>, not nested inside your gallery container — CSS custom properties only inherit down real DOM ancestry, so applying shoji-light (or any --shoji-* override) to the gallery container itself has no effect on the lightbox. Apply it to <body>, <html>, or another real ancestor of both the gallery container and where Shoji mounts:

<body class="shoji-light">
  ...
  <div id="gallery">...</div>
</body>

Override any property the same way — globally on :root or body:

body {
  --shoji-color-accent: #ff6b4a;
  --shoji-duration: 200ms;
}

There is currently no general way to scope a theme to just one gallery instance on a page with multiple instances — <body>-level classes/overrides apply to all of them; that would need a new GalleryOptions field (e.g. a className option applied directly to .shoji-outer) that doesn't exist yet. backdropOpacity (Settings guide) is the one narrow exception — it sets --shoji-backdrop-opacity directly on that instance's own .shoji-outer, so it's already per-instance today.

Colors

Property Dark (default) Light (.shoji-light)
--shoji-color-bg #0b0b0d #fafafa
--shoji-color-fg #f5f5f5 #111111
--shoji-color-accent #4da3ff — same in both themes; also drives --shoji-focus-ring and the autoplay progress bar (--shoji-progress)
--shoji-progress var(--shoji-color-accent) — the autoplay progress bar's own fill color, independently overridable without touching --shoji-color-accent itself.
--shoji-color-backdrop #000000 #ffffff
--shoji-backdrop-opacity 1 1
The backdrop's real alpha is --shoji-backdrop-opacity, not baked into --shoji-color-backdrop — --shoji-color-backdrop is a fully opaque hue by default, so the opacity value alone controls transparency, and 1 is genuinely, fully opaque. A custom theme can still use an rgba()/gradient value for --shoji-color-backdrop instead if wanted (it composites multiplicatively with the opacity value, CSS opacity semantics). Set --shoji-backdrop-opacity directly here, or via GalleryOptions.backdropOpacity (Settings guide), whichever is more convenient for a given integration.
--shoji-color-toolbar-bg rgba(20, 20, 22, 0.85) rgba(255, 255, 255, 0.9)
--shoji-color-disabled rgba(245, 245, 245, 0.35) rgba(17, 17, 17, 0.35)

Motion

Property Default Used for
--shoji-duration 300ms (forced to 0ms under prefers-reduced-motion: reduce) Open/close, slide transitions, layout FLIP animations.
--shoji-icon-swap-duration 150ms (forced to 0ms under prefers-reduced-motion: reduce) Cross-fade for a toolbar icon that reflects live state — Fullscreen's enter/exit, Autoplay's play/pause, Zoom's actual-size expand/contract. A dedicated var, not a reuse of --shoji-duration: a snappy state flip reads differently from a UI transition on the open/close scale.
--shoji-easing cubic-bezier(0.4, 0, 0.2, 1) Open/close/zoom-transition curve.
--shoji-momentum-easing cubic-bezier(0.2, 0, 0, 1) Drag-release settle animations specifically — a snap tuned for "released mid-motion," distinct from --shoji-easing.

Spacing & typography

Property Default
--shoji-spacing-sm 0.5rem
--shoji-spacing-md 1rem
--shoji-spacing-lg 1.5rem
--shoji-font-family System-font stack (San Francisco / Segoe UI / Roboto per platform) — no webfont download. Applied once on the dialog and inherited by everything inside it.
--shoji-focus-ring 2px solid var(--shoji-color-accent)
--shoji-caption-max-height calc(100% - var(--shoji-toolbar-height, var(--shoji-provider-video-top-inset)) - var(--shoji-spacing-md) - var(--shoji-spacing-sm)) — set on .shoji-dialog, not :root
--shoji-caption-collapsed-max-height max(calc(50% - 22px - var(--shoji-spacing-md) - var(--shoji-spacing-sm)), calc(1.6em + 2 * var(--shoji-spacing-sm))) — set on :root
--shoji-caption-max-width calc(100% - 2 * var(--shoji-spacing-md)); above a 768px viewport, min(calc(100% - 2 * var(--shoji-spacing-md)), max(24vw, 14rem))
--shoji-toolbar-height set by Gallery itself (a live measurement) — not meant to be overridden
--shoji-provider-video-top-inset calc(44px + 2 * var(--shoji-spacing-sm))

--shoji-caption-max-height caps how tall a caption can grow before it scrolls internally — it grows naturally with its content, only capping (and only then scrolling) once it would reach up under the toolbar. It's set on .shoji-dialog rather than :root deliberately: a custom property's nested var() references resolve at the element where that property is declared, not wherever a descendant later uses it, so it has to live on the same element --shoji-toolbar-height (the toolbar's real, live-measured height, kept up to date by a ResizeObserver in Gallery.ts — correct even if a busy toolbar wraps to multiple rows on a narrow viewport) is actually set on for the substitution to see it. --shoji-caption-collapsed-max-height is the collapsed default applied on top of that cap — as tall as the caption can grow while staying clear of the vertically-centered nav arrows sharing its left edge, floored at roughly one line so a pathologically short dialog can't collapse it to zero. A caption tall enough to still clip past both gets .shoji-caption--truncated and its own modal (--shoji-z-caption-modal below) to read the rest. --shoji-caption-max-width caps how wide it can grow before wrapping — spans (almost) the full dialog width on a narrow/mobile viewport, unchanged from the original behavior, but narrows to roughly a quarter of the viewport above a 768px breakpoint (floored at 14rem so it stays legible in a smaller desktop window) — so a long caption on a wide screen reads as a caption, not a bar spanning the dialog. --shoji-provider-video-top-inset reserves space at the top of a provider embed (YouTube etc.) matching the toolbar's own height, so the embed's hover-revealed title bar never sits under it — also the caption height cap's own fallback, before the real measurement above has landed (normally one animation frame after mount). That one doesn't apply to HTML5 video, which has no top-of-player UI to protect against.

Loading spinner

Property Default
--shoji-spinner-size 40px
--shoji-spinner-thickness 3px

Shown over a slide while its media is still loading; ring color is --shoji-color-disabled.

Z-index layers

No !important anywhere — a documented, layered scale instead:

Property Value Layer
--shoji-z-base 9000 .shoji-outer's own page-level z-index — against the rest of the host page, not the layers below (which only order themselves against each other, inside the stacking context this establishes). Override this if your page's own stack goes higher (a sticky navbar/toast container above 9000, say).
--shoji-z-backdrop 0 The dimming backdrop behind the dialog.
--shoji-z-slides 10 Slide media.
--shoji-z-overlay 20 Plugin overlays mounted via ctx.ui.overlay().
--shoji-z-toolbar 30 Toolbar (close/prev/next + plugin buttons).
--shoji-z-toast 40 Toasts.
--shoji-z-caption-modal 50 A truncated caption's full-text modal — above everything else in the dialog, a nested dialog of its own.

Layout plugin properties

--shoji-layout-column-width, --shoji-layout-gutter, and --shoji-layout-tile-aspect are set internally by the plugin per relayout (its columnWidth/gutter JS options feed into these — see LayoutOptions in the API reference) — but overridable the same way as any custom property. The --shoji-layout-heading-* family styles section heading typography and is plain CSS, never JS-driven.

Property Default
--shoji-layout-column-width 240px
--shoji-layout-gutter 8px
--shoji-layout-tile-aspect 1
--shoji-layout-heading-font-family inherit
--shoji-layout-heading-title-font-size 0.875rem
--shoji-layout-heading-title-font-weight 400
--shoji-layout-heading-title-color #767676
--shoji-layout-heading-subtitle-font-size 0.875rem
--shoji-layout-heading-subtitle-font-weight 700
--shoji-layout-heading-subtitle-color inherit

.shoji-layout-heading itself deliberately has no font-size custom property of its own — only its -title-/-subtitle- children do; a host targeting .shoji-layout-heading directly in CSS can already set a shared base size for both, so a redundant third property would only invite drift between the two ways of doing the same thing.

ActiveThumbnail plugin properties

--shoji-active-thumbnail-border-color drives the opt-in highlight: true styling (see Plugins) — set per-element by the plugin from its borderColor JS option (default 'blue'), and again to transparent if highlightDuration fades it out. Overridable with plain CSS like any other custom property; irrelevant when highlight is left at its default (false) — nothing reads it then.

--shoji-active-thumbnail-fade-duration controls how long that fade-to-transparent transition itself takes — set per-element from the highlightFadeDuration JS option (default 800ms), a dedicated property rather than reusing core's own --shoji-duration, since a deliberately slow, visible fade calls for a different default than an ordinary UI transition. Only meaningful alongside highlightDuration — nothing ever animates toward transparent without it.

Icons

Inline SVG sprites, stroke: currentColor — they inherit --shoji-color-fg automatically and are overridable per-button via each plugin's icon option (see Plugins).