OptionaltypeDefault 'justified'.
OptionalgutterOptionalaspecttype: 'grid' only — every cell's shape, cropped to fit via
object-fit: cover; default 1 (square). Deliberately not each
photo's own aspect ratio: plain CSS grid sizes a row's height to its
tallest cell, so per-item ratios leave a visible blank gap under every
shorter tile sharing a row with a taller one. Masonry/justified are
the two types that preserve true per-photo aspect ratios instead (via
their own JS packing, which doesn't have this row-height problem).
Optionalorientationtype: 'masonry' only. 'vertical' (default): fixed-width columns,
tiles stack top-to-bottom, ragged bottom edge. 'horizontal': fixed-
height rows, tiles pack left-to-right, ragged right edge — the direct
transpose. Both wrap/grow within the container and scroll vertically
only, same as every other layout type; neither ever needs horizontal
scroll. See rowHeight for 'horizontal''s one fixed input, and
justified (a different type entirely) for the opposite tradeoff —
width forced flush, height left to solve for.
OptionalcolumnsOptionalcolumnOptionalminOptionalmaxOptionalfillOptionalrowtype: 'justified', or type: 'masonry', orientation: 'horizontal'. For horizontal masonry this is the row's exact, fixed height (see orientation's doc comment) — minRowHeight/maxRowHeight don't apply there, since nothing is solved/clamped, only for justified, where it's a target a solved-for height converges near.
OptionalminOptionalmaxOptionallastOptionalanimateOptionalbreakpointsContainer-width → partial overrides, applied on top of the base options
above whenever the container's current width is <= that key (the
narrowest matching key wins if more than one applies). Re-evaluated on
every relayout, so crossing a threshold while resizing takes effect
live. Cannot override type or orientation — swapping the DOM
structure/positioning strategy live is a bigger feature than a sizing
tweak; use a media query + two Shoji instances if that's genuinely
needed.
OptionalgroupDerives a section key per item (e.g. a formatted date) — a heading is inserted whenever the key changes between consecutive items, real "Today / Yesterday / March 2024" (Google Photos-style) sectioning. Items must already be grouped consecutively; a key reappearing later after a different key started a new section anyway.
Section boundaries behave differently per type: grid's heading is a
full-span (grid-column: 1 / -1) element, native CSS grid gives it its
own row for free. masonry's heading is a full-width blocking element
too — each section is its own fresh column-packing pass, no two
sections ever share visual space. justified is the odd one out, by
design (this is the actual Google Photos behavior, not a simplified
approximation of it): sections do not force a row break. Row
composition ignores section boundaries entirely — one continuous
layoutJustified pass packs every item regardless of section, so a
section with too few items to reach a full row's target width (a
single lone photo, say) shares that row with whatever comes right
after it, instead of getting stretched into its own awkward
almost-empty row. A heading only marks where a new section starts:
a compact, inline label sized to its own content (not full width),
positioned at the x-coordinate of the tile that starts its section,
in a slim label-band above whichever row that tile landed on — more
than one label can share the same band if more than one section
happens to start within the same row (see renderHeading's
{title, subtitle} form for exactly this — that's what the reference
behavior actually looks like).
v1 scope cuts vs. DESIGN.md §5.2's full spec: (1) any items change
while groupBy is set does a full relayout, same simplification
justified's non-grouped path already makes for appends (§5.4) — the
"merge across load boundaries" incremental optimization is deferred;
(2) headings are always <h2>, not a configurable level; (3) not
supported with orientation: 'horizontal' (warns, ignored) —
DESIGN.md itself calls that pairing rare.
OptionalrenderCustomizes a heading's content; default renders the key as plain text. Three return shapes:
string — plain text (default behavior, just with custom content).HTMLElement — full control, own tag/children; the plugin adds its
own class/positioning on top of whatever's returned, nothing else.{ title: string; subtitle?: string } — the built-in structured
form (bold title + a smaller, muted subtitle, e.g. a date + a
location) — the only form the plugin can apply automatic overflow
handling to (see headingOverflow), since it owns the DOM structure
(a raw HTMLElement is opaque to it, no safe way to guess what's
droppable).Optionalheadingtype: 'justified' + the { title, subtitle } structured heading
form only — controls what happens when a compact label doesn't fit
its available width (the gap until the next section's label sharing
its row, or the container edge). Ignored otherwise: grid/masonry
headings are full-width and this kind of squeeze can't happen there;
a raw renderHeading-returned HTMLElement/string is left exactly
as returned regardless, since the plugin has no safe way to guess
what's droppable in an opaque element.
'show' (default) — always render the full title + subtitle, one
line, however wide; a label can visually run past the next
section's label or the container edge if the content is long
enough. The simple, predictable default — nothing gets silently
dropped or reflowed.'fit' — never wraps a label's text. First the subtitle is dropped
if title+subtitle together don't fit; if the title alone still
doesn't fit next to whatever's sharing its row, that section is
pushed to start a fresh row instead, giving its label the room it
needs — row composition responds to label width, not just label
content. Real Google Photos behavior; opt in when labels regularly
need to share tight row space. The one unavoidable exception is a
single label wider than the entire container even alone on its own
row — there, nothing left to break onto, so the label is
ellipsis-truncated (still one line, never wrapped, never
overflowing).'wrap' — the in-between: same continuous row-packing as 'show'
(row composition never reacts to label width, unlike 'fit' — no
section is ever pushed to a fresh row), but a label that doesn't fit
its available width (same gap 'fit' measures) wraps its own text
onto additional lines instead of running past it. Height is capped
at maxRowHeight (or its default, 360) — a label tall enough to hit
that cap ellipsis-truncates its last visible line rather than
growing the row further. Real Google Photos behavior for its date
headers; pick this over 'fit' when you'd rather a label take an
extra line than shove a whole section onto its own row.OptionalstickyPins the active section's heading via position: sticky. Only
implemented for type: 'grid', where headings sit in normal document
flow — masonry/justified headings are absolutely positioned by JS (like
their tiles), and position: sticky doesn't do anything meaningful
layered onto an already explicitly-positioned element. Warns and is
ignored for those two types.
OptionalautoWhen an item is missing width/height, measure its tile's own <img>
once it loads (naturalWidth/naturalHeight) and correct that tile's
aspect ratio in place, rather than leaving it at the permanent 4:3
placeholder fallback. Default true.
The first layout pass never waits on this — it always uses the 4:3
placeholder immediately, same as before this option existed, so opening
the gallery is never blocked on image loads (DESIGN.md §5.2's
never-measure-mid-layout-pass rule still holds for that first pass).
This only ever corrects a placeholder after the fact, once a real
measurement lands — the same "async correction after an initial
synchronous pass" pattern already used for a containerWidth <= 0
container (§5.4) and the zoom transition's origin-rect race (§2.3b),
extended to per-item dimensions. Multiple corrections landing close
together (e.g. several cached images resolving in the same tick) are
coalesced into one relayout via requestAnimationFrame, not one
relayout per image.
Effectively does automatically, for thumb/poster/src, what a host
would otherwise have to do by hand before constructing the gallery
(new Image(); img.onload = () => ...) — set false to keep the old
behavior (permanent 4:3 fallback, no self-correction) if you'd rather
guarantee zero extra relayouts under any circumstance.
DESIGN.md §5 —
type: 'grid' | 'masonry' | 'justified'; masonry supports bothorientations,breakpoints(§5.4), andgroupBy/headings (§5.2; see that option's own doc comment for the v1 scope cuts vs. the full spec, including whygroupByitself is excluded fromorientation: 'horizontal'). Deliberately still deferred: jump-free prepend coordination (§5.2's scroll-anchoring handoff needs the not-yet-built scroll plugin on the other end — prepending today triggers a full relayout, which can shift scroll position if content above the fold grew).