Panel
Panel
Caption and details in a side panel, which moves under the image on narrow screens.
A real lightbox, open inside the frame. Use ←→ to page and Esc to close it.
import { createLightbox } from "@reuse-sh/lightbox";
import "@reuse-sh/lightbox/styles.css";
import "./lightbox-style.css";
import { addChrome } from "./lightbox-chrome.js";
const lightbox = createLightbox({
className: "lightbox-style",
});
lightbox.open(photos, 0);
addChrome(lightbox, photos);import { useLightbox } from "@reuse-sh/lightbox/react";
import { useRef } from "react";
import "@reuse-sh/lightbox/styles.css";
import "./lightbox-style.css";
import { addChrome } from "./lightbox-chrome.js";
export function Gallery({ photos }) {
const lightbox = useLightbox({
className: "lightbox-style",
});
const root = useRef(null);
const open = (index) => {
const images = root.current.querySelectorAll("img");
const items = photos.map((photo, i) => ({ ...photo, origin: images[i] }));
lightbox.open(items, index);
addChrome(lightbox, items);
};
return (
<div ref={root}>
{photos.map((photo, index) => (
<button key={photo.src} onClick={() => open(index)}>
<img src={photo.src} alt={photo.alt} />
</button>
))}
</div>
);
}lightbox-style.css
.lightbox-style {
--lightbox-pad: 16px;
--lightbox-side: 0px;
--lightbox-caption: 0px;
--lightbox-strip: 0px;
--lightbox-bar: 0px;
--lightbox-under: 0px;
--lightbox-bottom: calc(var(--lightbox-caption) + var(--lightbox-strip) + var(--lightbox-bar) + var(--lightbox-under));
--lightbox-ring: 0 0 0 0.5px rgb(0 0 0 / 10%), 0 1px 2px rgb(0 0 0 / 6%), 0 2px 6px rgb(0 0 0 / 4%);
--reuse-lightbox-background: #fcfcfc;
--reuse-lightbox-foreground: #1f1f1f;
--reuse-lightbox-control: #fff;
}
.lightbox-style *, .lightbox-style *::before, .lightbox-style *::after { box-sizing: border-box; }
.lightbox-style .reuse-lightbox-viewport {
width: calc(100% - var(--lightbox-side));
height: calc(100% - var(--lightbox-bottom));
}
.lightbox-style .reuse-lightbox-button {
width: 40px;
height: 40px;
padding: 0;
color: inherit;
box-shadow: var(--lightbox-ring);
backdrop-filter: none;
transition: background-color 150ms ease, transform 160ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms ease;
}
.lightbox-style .reuse-lightbox-button svg { width: 18px; height: 18px; }
.lightbox-style .reuse-lightbox-button:hover { background: #f4f4f4; }
.lightbox-style .reuse-lightbox-button:active:not(:disabled) { transform: scale(0.95); }
.lightbox-style .reuse-lightbox-button:disabled { opacity: 0.3; }
.lightbox-style .reuse-lightbox-close, .lightbox-style .reuse-lightbox-zoom { top: var(--lightbox-pad); }
.lightbox-style .reuse-lightbox-close { right: var(--lightbox-pad); }
.lightbox-style .reuse-lightbox-zoom { right: calc(var(--lightbox-pad) + 48px); }
.lightbox-style .reuse-lightbox-footer {
right: calc(var(--lightbox-side) + var(--lightbox-pad));
bottom: calc(var(--lightbox-under) + var(--lightbox-pad));
left: var(--lightbox-pad);
gap: 10px;
}
.lightbox-style .reuse-lightbox-counter {
padding: 0;
background: none;
backdrop-filter: none;
font-size: 13px;
}
.lightbox-style .reuse-lightbox-prev,
.lightbox-style .reuse-lightbox-next { top: calc((100% - var(--lightbox-bottom)) / 2 - 20px); }
.lightbox-style .reuse-lightbox-prev { left: var(--lightbox-pad); }
.lightbox-style .reuse-lightbox-next { right: calc(var(--lightbox-side) + var(--lightbox-pad)); }
.lightbox-style .reuse-lightbox-counter {
position: fixed;
z-index: 2;
top: var(--lightbox-pad);
left: var(--lightbox-pad);
display: grid;
height: 40px;
padding: 0 14px;
place-items: center;
border-radius: 999px;
background: #fff;
box-shadow: var(--lightbox-ring);
font-weight: 500;
}
.lightbox-style .reuse-lightbox-zoom { display: none; }
.lightbox-style .reuse-lightbox-caption { display: none; }
.lightbox-style { --lightbox-side: 240px; }
.lightbox-style-panel {
position: absolute;
z-index: 1;
top: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
width: var(--lightbox-side);
padding: 72px 24px 24px;
background: #fff;
box-shadow: -1px 0 0 rgb(0 0 0 / 6%);
color: #1f1f1f;
}
.lightbox-style-panel :where(h2, p) { margin: 0; }
.lightbox-style-panel h2 { font-size: 19px; font-weight: 560; line-height: 26px; letter-spacing: -0.01em; }
.lightbox-style-panel-place { margin-top: 2px; color: #8a8a8a; font-size: 13.5px; line-height: 20px; }
.lightbox-style-panel-text { margin-top: 16px; color: #4a4a4a; font-size: 13.5px; line-height: 21px; }
@media (max-width: 559px) {
.lightbox-style { --lightbox-pad: 12px; }
}
@media (max-width: 559px) {
.lightbox-style { --lightbox-side: 0px; --lightbox-under: 112px; }
.lightbox-style-panel {
top: auto;
left: 0;
width: auto;
height: var(--lightbox-under);
padding: 16px;
box-shadow: 0 -1px 0 rgb(0 0 0 / 6%);
}
.lightbox-style-panel h2 { font-size: 16px; line-height: 22px; }
.lightbox-style-panel-place { font-size: 13px; }
.lightbox-style-panel-text { margin-top: 6px; font-size: 13px; line-height: 19px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
}
@media (prefers-reduced-motion: reduce) {
.lightbox-style, .lightbox-style * { transition-duration: 0ms !important; }
}lightbox-chrome.js
export function addChrome(lightbox, items) {
const dialog = document.querySelector("dialog.lightbox-style");
const part = (name) => dialog.querySelector(`.reuse-lightbox-${name}`);
const syncs = [];
const cleanups = [];
const panel = document.createElement("aside");
panel.className = "lightbox-style-panel";
panel.innerHTML = `<h2></h2><p class="lightbox-style-panel-place"></p><p class="lightbox-style-panel-text"></p>`;
dialog.append(panel);
const [title, place, text] = panel.children;
syncs.push((state) => {
const item = items[state.index];
title.textContent = item.caption;
place.textContent = `${state.index + 1} of ${state.count}`;
text.textContent = `${item.alt}.`;
});
const sync = (state) => syncs.forEach((fn) => fn(state));
sync(lightbox.getState());
const stop = lightbox.subscribe((state) => {
if (state.isOpen) sync(state);
else if (!state.isClosing) {
stop();
cleanups.forEach((fn) => fn());
}
});
}