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