Shoji API Reference
    Preparing search index...

    Interface LayoutOptions

    DESIGN.md §5 — type: 'grid' | 'masonry' | 'justified'; masonry supports both orientations, breakpoints (§5.4), and groupBy/headings (§5.2; see that option's own doc comment for the v1 scope cuts vs. the full spec, including why groupBy itself is excluded from orientation: '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).

    interface LayoutOptions {
        type?: "grid" | "masonry" | "justified";
        gutter?: number | { x: number; y: number };
        aspectRatio?: number;
        orientation?: "horizontal" | "vertical";
        columns?: number | "auto";
        columnWidth?: number;
        minColumnWidth?: number;
        maxColumnWidth?: number;
        fill?: "shortest" | "ordered";
        rowHeight?: number;
        minRowHeight?: number;
        maxRowHeight?: number;
        lastRow?: "left" | "justify" | "hide";
        animate?: boolean;
        breakpoints?: Record<
            number,
            Omit<LayoutOptions, "type" | "orientation" | "breakpoints">,
        >;
        groupBy?: (item: GalleryItem) => string;
        renderHeading?: (
            key: string,
            items: GalleryItem[],
        ) => string | HTMLElement | { title: string; subtitle?: string };
        headingOverflow?: "show" | "fit" | "wrap";
        stickyHeadings?: boolean;
        autoMeasure?: boolean;
    }
    Index
    type?: "grid" | "masonry" | "justified"

    Default 'justified'.

    gutter?: number | { x: number; y: number }
    aspectRatio?: number

    type: '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).

    orientation?: "horizontal" | "vertical"

    type: '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.

    columns?: number | "auto"
    columnWidth?: number
    minColumnWidth?: number
    maxColumnWidth?: number
    fill?: "shortest" | "ordered"
    rowHeight?: number

    type: '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.

    minRowHeight?: number
    maxRowHeight?: number
    lastRow?: "left" | "justify" | "hide"
    animate?: boolean
    breakpoints?: Record<
        number,
        Omit<LayoutOptions, "type" | "orientation" | "breakpoints">,
    >

    Container-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.

    groupBy?: (item: GalleryItem) => string

    Derives 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.

    renderHeading?: (
        key: string,
        items: GalleryItem[],
    ) => string | HTMLElement | { title: string; subtitle?: string }

    Customizes 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).
    headingOverflow?: "show" | "fit" | "wrap"

    type: '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.
    stickyHeadings?: boolean

    Pins 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.

    autoMeasure?: boolean

    When 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.