燈箱藝廊(React)
縮圖牆點擊放大的全螢幕燈箱 React 元件:圖片從縮圖以共享元素 morph 進場,支援滾輪/捏合縮放與拖曳平移、雙擊放大、滑動與鍵盤切換、計數與說明列、底部縮圖列,Esc 或點背景關閉。
這是 WebberUI Pro 元件
上線活動期間免費:註冊或登入後,在上方預覽區按「複製安裝指令」就能直接安裝,不需付費、不用綁信用卡。下方那條指令未登入時會回 401。
縮圖牆(CSS grid、hover 輕放大、固定 4:3 比例)點一張就開全螢幕燈箱:大圖以共享元素從縮圖原位放大進場,關閉時縮回原位。燈箱內以 pointer 事件實作完整手勢——滾輪或雙指捏合以游標/兩指中點為中心縮放、放大後單指拖曳平移(範圍夾在圖片邊界內)、縮放為 1 時左右滑動超過 80px 切換、雙擊在 1 倍與 2 倍間切換;工具列有計數、放大/縮小與關閉,左右箭頭與 ←/→ 鍵換圖,底部有說明列與可點的縮圖列。開啟時鎖住頁面捲動、焦點移到關閉鈕並在燈箱內循環,關閉後焦點回到原縮圖。
載入預覽⋯
npx shadcn@latest add "https://webberui.com/r/lightbox-gallery.json?t=<安裝 token>"Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
3
4
<LightboxGallery />
安裝
npx shadcn@latest add "https://webberui.com/r/lightbox-gallery.json?t=<安裝 token>"或在 components.json 設定 registries 後,改用 @webberui/lightbox-gallery 安裝。
使用
import { LightboxGallery, type LightboxImage } from "@/components/ui/lightbox-gallery";
const images: LightboxImage[] = [
{ id: "a", src: "/photos/a.jpg", thumb: "/photos/a-thumb.jpg", alt: "海邊日出", caption: "清晨五點的東岸" },
{ id: "b", src: "/photos/b.jpg", alt: "山谷雲海" },
{ id: "c", src: "/photos/c.jpg", alt: "老街午後", caption: "屋簷下的光影" },
];
// 非受控:點縮圖就開,元件自己管理開關與索引
<LightboxGallery images={images} columns={3} loop />
// 受控:由外部按鈕開到指定那一張
const [open, setOpen] = useState(false);
const [index, setIndex] = useState(0);
<LightboxGallery
images={images}
open={open}
onOpenChange={setOpen}
index={index}
onIndexChange={setIndex}
showThumbnails={false}
zoomMax={6}
/>Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
images | LightboxImage[] | — | 圖片清單;id 需唯一 |
columns | number | 3 | 縮圖牆欄數 |
gap | number | 8 | 縮圖牆格距(px) |
startIndex | number | 0 | 非受控模式的初始索引(首次掛載時的目前圖片) |
index | number | — | 受控的目前索引;不提供時由元件內部管理 |
onIndexChange | (index: number) => void | — | 目前索引變更時回呼(點縮圖、切換、滑動皆會觸發) |
open | boolean | — | 受控的燈箱開關;不提供時由元件內部管理 |
defaultOpen | boolean | false | 非受控模式的初始開關 |
onOpenChange | (open: boolean) => void | — | 燈箱開關變更時回呼(受控與非受控皆會觸發) |
loop | boolean | false | 是否在頭尾循環切換 |
showThumbnails | boolean | true | 是否顯示燈箱底部縮圖列 |
showCounter | boolean | true | 是否顯示「3 / 12」計數 |
zoomMax | number | 4 | 最大縮放倍率 |
labels | Partial<LightboxGalleryLabels> | — | 自訂介面文字(部分覆寫即可) |
className | string | — | 透傳到最外層容器 |
LightboxImage
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
id | string | — | 唯一識別碼;縮圖與燈箱大圖的共享元素動畫以此配對 |
src | string | — | 大圖來源(需具備固有尺寸:一般點陣圖或帶 width/height 的 SVG) |
alt | string | — | 替代文字(無障礙必填),切換時也會被螢幕閱讀器朗讀 |
caption | string | — | 燈箱底部說明列文字 |
thumb | string | — | 縮圖來源;未提供時縮圖牆與燈箱縮圖列都用 src |
LightboxGalleryLabels
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
dialog | string | "圖片燈箱" | 燈箱對話框的 aria-label |
open | string | "放大檢視" | 縮圖牆按鈕無障礙名稱的後綴,組成「替代文字(放大檢視)」 |
close | string | "關閉" | 關閉按鈕 |
previous | string | "上一張" | 上一張按鈕 |
next | string | "下一張" | 下一張按鈕 |
zoomIn | string | "放大" | 放大按鈕 |
zoomOut | string | "縮小" | 縮小按鈕 |
thumbnails | string | "縮圖列" | 燈箱底部縮圖列的 aria-label |
另外具名匯出 LightboxImage、LightboxGalleryLabels 與 LightboxGalleryProps 型別。
細節
- 共享元素進場:開啟燈箱那一張的大圖與縮圖共用 Motion
layoutId,從縮圖原位放大進場;關閉時若仍停在同一張,會縮回原縮圖,切換過圖片後則淡出。之後切換的圖片以水平滑動進出場,切換時縮放與平移一併重置。 - 手勢:以 pointer 事件實作,滑鼠、觸控、觸控筆行為一致。滾輪(含帶
ctrlKey的觸控板捏合)以游標為中心縮放;雙指捏合追蹤兩個 pointer 的距離與中點,可邊縮放邊平移;縮放大於 1 時單指拖曳平移,平移範圍夾在圖片邊界內;縮放為 1 時單指水平位移超過 80px 切換(到頭尾且不循環時加阻力後彈回);300ms 內、24px 內的第二次點按視為雙擊,在 1 倍與 2 倍間切換;點按圖片以外的背景關閉。 - 鍵盤:←/→ 切換、Home/End 跳頭尾、Esc 關閉、
+/-縮放、0回到 1 倍。 - 尺寸:大圖以
max-width: 100%; max-height: 100%等比縮放至舞台內,不放大小於舞台的圖;src需要有固有尺寸(點陣圖皆有;SVG 請帶width/height)。開啟時會預先載入前後一張。 - 傳送門與捲動鎖定:燈箱以
createPortal掛在document.body,避免被祖先的transform/overflow影響;開啟時在 effect 內把document.body.style.overflow設為hidden,關閉或卸載時還原原值。
可及性
- 燈箱為
role="dialog"、aria-modal="true",aria-label取自labels.dialog,有說明文字時以aria-describedby指向說明列;開啟時焦點移到關閉鈕,Tab/Shift+Tab 在燈箱內循環,關閉後焦點回到原本點擊的縮圖 - 縮圖牆每格都是
button(aria-haspopup="dialog"),無障礙名稱為「替代文字(放大檢視)」;工具列、箭頭與縮圖列按鈕都有aria-label,目前縮圖以aria-current標示;切換時aria-live="polite"區域朗讀「第 n 張,共 N 張:替代文字」 - 所有互動皆有鍵盤對應(方向鍵切換、Esc 關閉、
+/-/0縮放),按鈕具focus-visible外框 - 使用者系統開啟「減少動態效果」時,共享元素 morph 改為淡入淡出、水平滑動改為淡入淡出、縮放不做補間直接到位、縮圖列改為即時捲動,功能不受影響