Lightbox

A full-viewport image viewer for a gallery. Not a trigger itself: render your own thumbnails and drive index (controlled or uncontrolled) from their click handlers.

Real use · thumbnail grid opens a full-viewport viewer

Using Lightbox

Silica ships one design system through three paths. They share a class vocabulary but are not interchangeable — pick the one that matches what you are building.

Plain HTML — CSS classes

The Tailwind v4 plugin. Works in any framework or none — Rails, Django, PHP, Go, a static file. This path ships no JavaScript, so Lightbox is styled but inert: its behavior comes from modal. For that behavior with no framework, use the node-tree path below.

/* in your CSS */
@import "tailwindcss";
@plugin "@wizeworks/silicaui";

Root class lightbox-backdrop, with 8 modifiers:

  • .lightbox-backdrop
  • .lightbox-caption
  • .lightbox-close
  • .lightbox-counter
  • .lightbox-image
  • .lightbox-nav
  • .lightbox-nav-next
  • .lightbox-nav-prev
  • .lightbox-popup

React

Typed components over those same classes, with behavior and accessibility from Base UI.

npm i @wizeworks/silicaui-react
import { Lightbox } from "@wizeworks/silicaui-react";

Props

PropTypeRequiredDescription
itemsLightboxItem[]yes—
indexnumber | nullnoControlled open index; null (or undefined while uncontrolled) is closed.
defaultIndexnumber | nullno—
onIndexChange(index: number | null) => voidno—
loopbooleannoWrap past the first/last item with the nav arrows. Default true.
classNamestringno—

Source of the demo above

The exact file that renders the examples above, so it can never drift from them. It imports a few of this site’s own layout helpers (../lib/…), so read it for the component calls rather than copying it whole.

Show source
import { useState } from "react";
import { Lightbox, type LightboxItem } from "@wizeworks/silicaui-react";
import { Section } from "../lib/Section";

// Stand-in "photos" (data URIs, always load offline): a gradient tile with a
// number, so the viewer/nav path is demonstrated without a network request.
function placeholder(n: number, from: string, to: string): string {
    return `data:image/svg+xml,${encodeURIComponent(
        `<svg xmlns='http://www.w3.org/2000/svg' width='800' height='600'>` +
            `<defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'>` +
            `<stop offset='0' stop-color='${from}'/><stop offset='1' stop-color='${to}'/>` +
            `</linearGradient></defs>` +
            `<rect width='800' height='600' fill='url(#g)'/>` +
            `<text x='400' y='330' font-size='180' font-family='sans-serif' font-weight='700' fill='rgba(255,255,255,.85)' text-anchor='middle'>${n}</text>` +
            `</svg>`,
    )}`;
}

const PHOTOS: LightboxItem[] = [
    { src: placeholder(1, "#6366f1", "#ec4899"), caption: "Photo 1" },
    { src: placeholder(2, "#f97316", "#eab308"), caption: "Photo 2" },
    { src: placeholder(3, "#10b981", "#06b6d4"), caption: "Photo 3" },
    { src: placeholder(4, "#8b5cf6", "#ec4899"), caption: "Photo 4" },
];

export function LightboxDemo() {
    const [index, setIndex] = useState<number | null>(null);

    return (
        <Section title="Real use · thumbnail grid opens a full-viewport viewer">
            <div className="grid grid-cols-4 gap-2">
                {PHOTOS.map((p, i) => (
                    <button
                        key={p.src}
                        type="button"
                        className="overflow-hidden rounded-field"
                        onClick={() => setIndex(i)}
                    >
                        <img src={p.src} alt="" className="block h-24 w-full object-cover" />
                    </button>
                ))}
            </div>
            <Lightbox items={PHOTOS} index={index} onIndexChange={setIndex} />
        </Section>
    );
}

Vanilla — node tree to HTML

Lightbox — trigger[i] opens slide[i] (positional pairing, like tabs); Counter reuses the title role (same "text this behavior keeps in sync" convention calendar's month label uses). Build a node tree, project it with toHtml(), and let the zero-dependency runtime hydrate it. This is the path for generated or user-authored documents — a site builder, CMS output, a static export.

npm i @wizeworks/silicaui-html @wizeworks/silicaui-behaviors
import { atom, toHtml } from "@wizeworks/silicaui-html";

const node = atom("Lightbox", "lightbox-backdrop", { /* props */ }, [/* children */]);
const markup = toHtml(node);

Styling on this path is the class string in the second argument — the same classes as the Plain HTML section above. There is no color prop here; write "lightbox-backdrop lightbox-backdrop-primary" rather than passing a colour as a prop.

Takes children
yes
Hydrated by
modal

Load @wizeworks/silicaui-behaviors on the page or this markup renders correctly and does nothing.

The full API for Lightbox, on every path, is available through the Silica MCP server.