Lightbox

一个全屏的图片与视频画廊Lightbox组件,基于 @reelkit/react-lightbox.

查看在线演示 →

特性

图片与视频
内置视频幻灯片支持
触摸手势
滑动翻页
滑动关闭
上滑关闭
键盘导航
方向键 + Escape
全屏
跨浏览器 API
过渡动画
滑动、淡入、翻转、放大
预加载
相邻图片预取
声音开关
逐张静音 / 取消静音
加载状态
转圈动画 + 自定义渲染
错误处理
错误图标 + 自定义渲染
Render Props
6 个可定制的渲染区域
Hooks
useVideoSlideRenderer + useFullscreen
URL 状态
可分享、可收藏的链接
幻灯片播放
按需开启的自动前进,可播放 / 暂停
下载
按需开启的按钮,保存当前项

安装

bash
npm install @reelkit/react-lightbox @reelkit/react lucide-react

别忘了引入样式:

typescript
import '@reelkit/react-lightbox/styles.css';
图标

默认控件使用 lucide-react 作为图标。如果你想换一套图标库,可以用 renderControls 和 renderNavigation 提供自己的实现。

快速上手

LightboxOverlay 组件以全屏方式展示图片。传入一组 LightboxItem 对象,并用一个可为 null 的索引控制显示。

tsx
import { useState } from 'react';
import { LightboxOverlay, type LightboxItem } from '@reelkit/react-lightbox';
import '@reelkit/react-lightbox/styles.css';

const images: LightboxItem[] = [
  {
    src: 'https://example.com/image1.jpg',
    title: 'Sunset',
    description: 'Beautiful sunset over the ocean',
  },
  {
    src: 'https://example.com/image2.jpg',
    title: 'Mountains',
  },
];

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

  return (
    <>
      {images.map((img, i) => (
        <img
          key={i}
          src={img.src}
          onClick={() => setIndex(i)}
        />
      ))}
      <LightboxOverlay
        isOpen={index !== null}
        images={images}
        initialIndex={index ?? 0}
        onClose={() => setIndex(null)}
      />
    </>
  );
}
LightboxPage.tsx
import { useState } from 'react';
import {
  LightboxOverlay,
  flipTransition,
  lightboxFadeTransition,
  lightboxZoomTransition,
  slideTransition,
  type LightboxItem,
} from '@reelkit/react-lightbox';
import type { TransitionTransformFn } from '@reelkit/react';
import '@reelkit/react-lightbox/styles.css';

const images: LightboxItem[] = [
  {
    src: '/cdn/samples/images/image-01.jpg',
    title: 'Mountain River',
    description: 'A beautiful mountain river flowing through the forest',
    width: 1600,
    height: 1000,
  },
  {
    src: '/cdn/samples/images/image-02.jpg',
    title: 'Snowy Peaks',
    description: 'Majestic snow-capped mountains reaching for the sky',
    width: 1000,
    height: 1600,
  },
  {
    src: '/cdn/samples/images/image-03.jpg',
    title: 'Foggy Forest',
    description: 'Misty morning in the dense forest',
    width: 1600,
    height: 900,
  },
  {
    src: '/cdn/samples/images/image-04.jpg',
    title: 'Ocean Waves',
    description: 'Powerful ocean waves crashing against the rocky shore',
    width: 900,
    height: 1400,
  },
  {
    src: '/cdn/samples/images/image-05.jpg',
    title: 'Autumn Path',
    description: 'A winding path through the autumn forest',
    width: 1600,
    height: 1067,
  },
  {
    src: '/cdn/samples/images/image-06.jpg',
    title: 'Coastal Cliffs',
    description: 'Dramatic coastal cliffs overlooking the deep blue sea',
    width: 1600,
    height: 1067,
  },
];

const transitions: { label: string; fn: TransitionTransformFn }[] = [
  { label: 'slide', fn: slideTransition },
  { label: 'fade', fn: lightboxFadeTransition },
  { label: 'flip', fn: flipTransition },
  { label: 'zoom-in', fn: lightboxZoomTransition },
];

export default function App() {
  const [index, setIndex] = useState<number | null>(null);
  const [transitionFn, setTransitionFn] = useState<TransitionTransformFn>(
    () => slideTransition,
  );

  return (
    <div style={{ padding: 16, background: '#f8fafc', minHeight: '100vh' }}>
      {/* Transition picker */}
      <div style={{ display: 'flex', gap: 6, marginBottom: 12 }}>
        {transitions.map((t) => (
          <button
            key={t.label}
            onClick={() => setTransitionFn(() => t.fn)}
            style={{
              padding: '6px 14px', borderRadius: 8, border: 'none', cursor: 'pointer',
              fontSize: 13, fontWeight: 500,
              background: transitionFn === t.fn ? '#6366f1' : '#e2e8f0',
              color: transitionFn === t.fn ? '#fff' : '#334155',
            }}
          >
            {t.label}
          </button>
        ))}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 }}>
        {images.map((img, i) => (
          <button
            key={i}
            onClick={() => setIndex(i)}
            style={{
              position: 'relative', aspectRatio: '4 / 3', borderRadius: 8,
              overflow: 'hidden', border: 'none', padding: 0, cursor: 'pointer',
              background: '#e2e8f0',
            }}
          >
            <img
              src={img.src}
              alt={img.title}
              style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
            />
          </button>
        ))}
      </div>
      <LightboxOverlay
        isOpen={index !== null}
        images={images}
        initialIndex={index ?? 0}
        onClose={() => setIndex(null)}
        transitionFn={transitionFn}
      />
    </div>
  );
}

想把图库以幻灯片播放并保存每张照片的原始文件,请查看图库幻灯片示例。

视频幻灯片(按需开启)

视频支持完全按需开启且可被 tree-shaking —— 只用图片时不会增加任何体积。引入 useVideoSlideRenderer 并把它的返回值接到 LightboxOverlay。这个 Hook 会自动处理加载状态、声音管理和视频生命周期。

tsx
import {
  LightboxOverlay,
  useVideoSlideRenderer,
  type LightboxItem,
} from '@reelkit/react-lightbox';
import '@reelkit/react-lightbox/styles.css';

const items: LightboxItem[] = [
  { src: '/photo.jpg', title: 'Photo' },
  {
    src: '/clip.mp4',
    type: 'video',
    poster: '/clip-thumb.jpg',
    title: 'Video Clip',
  },
];

function Gallery() {
  const [index, setIndex] = useState<number | null>(null);
  const isOpen = index !== null;
  const { renderSlide, renderControls, SoundProvider } =
    useVideoSlideRenderer(items);

  return (
    <SoundProvider>
      {/* thumbnails… */}
      <LightboxOverlay
        isOpen={isOpen}
        images={items}
        initialIndex={index ?? 0}
        onClose={() => setIndex(null)}
        renderSlide={renderSlide}
        renderControls={renderControls}
      />
    </SoundProvider>
  );
}
工作原理
  • 这个 Hook 返回 SoundProvider —— 把浮层包在它里面,静音开关才能生效
  • 幻灯片变为活动时视频会自动播放(默认静音)
  • 各幻灯片复用同一个 video 元素,以保证 iOS 上声音连续
  • 视频幻灯片上会自动出现声音按钮,带响应式的静音开关
  • 没有 type: 'video' 的条目按图片渲染(向后兼容)

自定义

自定义控件

使用 renderControls 替换默认的关闭按钮、计数器和全屏开关。可以和导出的子组件组合使用:

tsx
import {
  LightboxOverlay,
  CloseButton,
  Counter,
  FullscreenButton,
} from '@reelkit/react-lightbox';

<LightboxOverlay
  isOpen={isOpen}
  images={images}
  onClose={handleClose}
  renderControls={({ onClose, activeIndex, count, isFullscreen, onToggleFullscreen }) => (
    <div style={{ position: 'absolute', top: 12, left: 16, right: 16, display: 'flex', justifyContent: 'space-between', zIndex: 10 }}>
      <Counter currentIndex={activeIndex} count={count} />
      <div>
        <FullscreenButton isFullscreen={isFullscreen} onToggle={onToggleFullscreen} />
        <CloseButton onClick={onClose} />
      </div>
    </div>
  )}
/>

自定义信息浮层

使用 renderInfo 替换默认的标题 / 描述渐变层,或者传 renderInfo={() => null} 把它整个隐藏:

tsx
<LightboxOverlay
  isOpen={isOpen}
  images={images}
  onClose={handleClose}
  renderInfo={({ item, index }) => (
    <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: 16, background: 'linear-gradient(transparent, rgba(0,0,0,0.8))', color: '#fff', zIndex: 10 }}>
      <h3>{item.title}</h3>
      <p>{item.description}</p>
    </div>
  )}
/>

自定义导航

使用 renderNavigation 替换默认的上一张 / 下一张箭头:

tsx
<LightboxOverlay
  isOpen={isOpen}
  images={images}
  onClose={handleClose}
  renderNavigation={({ onPrev, onNext, activeIndex, count }) => (
    <div style={{ position: 'absolute', bottom: 24, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 12, zIndex: 10 }}>
      <button onClick={onPrev} disabled={activeIndex === 0}>Prev</button>
      <span>{activeIndex + 1} / {count}</span>
      <button onClick={onNext} disabled={activeIndex === count - 1}>Next</button>
    </div>
  )}
/>

自定义幻灯片

使用 renderSlide 可以完全自定义幻灯片内容。返回 null 则回退到默认的图片幻灯片:

tsx
<LightboxOverlay
  isOpen={isOpen}
  images={images}
  onClose={handleClose}
  renderSlide={({ item, index, size, isActive, onReady, onError }) => {
    // Custom CTA on last slide
    if (index === images.length - 1) {
      return (
        <div style={{ width: size[0], height: size[1], display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff' }}>
          <h2>View all photos</h2>
        </div>
      );
    }
    return null; // default image slide
  }}
/>

内容加载与错误处理

Lightbox会逐张跟踪加载和错误状态。内容加载时显示转圈动画;媒体失败时显示图片损坏图标。出错的 URL 会被缓存,再次访问时立刻显示错误而不重试。

生命周期回调

使用 renderSlide时,请调用这些回调来控制加载提示:

回调何时调用
onReady图片已加载,或视频已开始播放。会清除加载和错误状态。
onWaiting视频在播放途中正在缓冲。显示加载提示。
onError内容加载失败。显示错误浮层,并把该 URL 标记为损坏缓存起来。
tsx
// Inside renderSlide — wire callbacks to your custom media
renderSlide={({ item, index, size, isActive, onReady, onWaiting, onError }) => (
  <div style={{ width: size[0], height: size[1] }}>
    {item.type === 'video' ? (
      <video
        src={item.src}
        poster={item.poster}
        autoPlay={isActive}
        onCanPlay={onReady}
        onWaiting={onWaiting}
        onError={onError}
        style={{ width: '100%', height: '100%', objectFit: 'contain' }}
      />
    ) : (
      <img
        src={item.src}
        onLoad={onReady}
        onError={onError}
        style={{ width: '100%', height: '100%', objectFit: 'contain' }}
      />
    )}
  </div>
)}

自定义加载与错误界面

用自定义组件替换默认的转圈动画和错误图标:

tsx
<LightboxOverlay
  isOpen={isOpen}
  images={images}
  onClose={() => setIsOpen(false)}
  renderLoading={({ item, activeIndex }) => (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 10,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      color: '#fff', fontSize: 14,
    }}>
      Loading image {activeIndex + 1}...
    </div>
  )}
  renderError={({ item, activeIndex }) => (
    <div style={{
      position: 'absolute', inset: 0, zIndex: 10,
      display: 'flex', flexDirection: 'column',
      alignItems: 'center', justifyContent: 'center',
      gap: 12, color: 'rgba(255,255,255,0.5)',
    }}>
      <span style={{ fontSize: 48 }}>!</span>
      <span>Failed to load content</span>
    </div>
  )}
/>

URL 状态

查看在线演示 →

LightboxUrlOverlay 是一个独立组件,它的打开状态存放在地址栏里。用 useOverlayUrlState from @reelkit/react 构建控制器,再作为 controller传进去:参数指向某张幻灯片时画廊自己打开,参数消失时关闭。链接可以分享,返回键关闭的是画廊而不是离开页面。

内置的 key

可以用内置的 key 来寻址幻灯片 —— 把 urlIndexKey (按位置)或 urlStableIdKey (按稳定的 id)展开进控制器 —— 两者都从 @reelkit/react. See the URL 状态指南 和 核心 API.

tsx
import { useOverlayUrlState, urlIndexKey, urlStableIdKey } from '@reelkit/react';
import { LightboxUrlOverlay } from '@reelkit/react-lightbox';
import { Link } from 'react-router-dom';

const photo = useOverlayUrlState({
  param: 'photo',
  ...urlIndexKey(() => images.length),
});

// Opening is a link — the href is the open action. No open flag, no handler:
// the overlay reads the URL and opens itself.
{images.map((image, i) => (
  <Link key={image.src} to={`?photo=${i}`}>
    <img src={image.src} />
  </Link>
))}

<LightboxUrlOverlay controller={photo} images={images} />

这个 Hook 接受一个选项对象,返回一个 UrlStateController (带 set, index, value)。留着它就能编程式地控制: set 是浮层内部使用的底层写入(切换幻灯片,以及用 set(null) 关闭)。它同样可以编程式驱动浮层 —— set(index) 会打开它,效果和导航到该参数一样。不过打开时更推荐用链接:href 可以分享、能在新标签页打开、返回键会关闭它 —— 全都不用写处理函数。

完整的 useOverlayUrlState 选项(param, adapter, codec, locator):见 React API 参考.

LightboxUrlOverlay 本身只接受 controller (必填)、可选的 onClose,外加所有视觉和行为属性 LightboxOverlay 所接受的(images, ariaLabel, transitionFn、各种 render props 等等)—— 但没有 isOpen.

  • 打开会花掉一条历史记录。翻页则是替换它,所以滑一百次也不会多出记录 —— 退一步永远就是离开画廊。返回键关闭画廊,不会逐张后退。
  • 像 ?photo=3 这样的分享链接会把画廊直接打开到那一张。关闭随页面一起到达的链接时,会就地移除参数,而不是导航离开你的站点。
  • 只有从应用内部打开时返回键才会关闭 —— 因为那次链接压入了一条记录,返回就会弹回画廊。在新标签页里直接打开的分享链接背后没有历史,浏览器返回会离开站点;这时关闭按钮或 Escape 会就地移除参数,把你留在画廊上。自带的路由适配器能为同一页面内的链接压栈作证;无法说明记录如何到来的自定义适配器同样会就地关闭,历史里会留下一条重复的页面记录 —— 于是有一次返回看起来什么都没发生,但也不会重新打开任何东西。
  • 指向不存在幻灯片的参数 —— 过期的书签、手改的值 —— 会从 URL 中移除,而不是让地址栏继续声称一张打不开的幻灯片。

带路由的应用请传入适配器。 直接写历史会让路由器自己的 location 过期,下一次导航就会把参数丢掉。

tsx
import { useReactRouterUrlAdapter } from '@reelkit/react/react-router-url-adapter';

const adapter = useReactRouterUrlAdapter();
const photo = useOverlayUrlState({
  param: 'photo',
  adapter,
  ...urlIndexKey(() => images.length),
});

<LightboxUrlOverlay controller={photo} images={images} />

打开就是一个链接。 因为打开状态存放在 URL 里,缩略图就是一个普通链接 —— 不需要点击处理函数 —— 浏览器自带的行为也就免费到手:新标签页打开、复制地址、悬停预览。带路由的应用请用路由器的链接组件,让导航保持在客户端。

tsx
import { Link } from 'react-router-dom';

// The href is the open action — no onClick, no open flag.
{images.map((image, i) => (
  <Link key={image.src} to={`?photo=${i}`}>
    <img src={image.src} />
  </Link>
))}

<LightboxUrlOverlay controller={photo} images={images} />

分享链接请优先使用稳定身份。 索引是按位置的,所以收藏下来的 ?photo=3 在列表重新排序后就会打开另一张图片。 urlStableIdKey 按每个条目稳定的 id来寻址,扫描当前列表 —— 一次调用就覆盖了常见场景。

tsx
const photo = useOverlayUrlState({
  param: 'photo',
  ...urlStableIdKey({ items: () => images }),
});

<LightboxUrlOverlay controller={photo} images={images} />

传入 hashCodec: base64UrlCodec 即可把 URL 中的 id 做 base64url 编码 —— 这是可逆的混淆,不是加密哈希。

想按别的字段(比如 slug)来寻址,或者用 locateAsync给无限信息流翻页,就自己构建 codec/locator :

tsx
const photo = useOverlayUrlState({
  param: 'photo',
  codec: { decode: (raw) => raw, encode: (id) => id },
  locator: {
    locate: (id) => images.findIndex((x) => x.slug === id),
    identify: (index) => images[index].slug,
  },
});

<LightboxUrlOverlay controller={photo} images={images} />

无限或分页画廊。 同步的 locate 只能回答已经加载过的图片 —— 只加载了 20 张时,指向第 400 张的分享链接就查不到。 locateAsync 是兜底,只有在 locate 未命中时才调用。

快捷键

只想按条目的 id来寻址?那就不必手写编解码器和定位器 —— 直接把 locateAsync 传给 urlStableIdKey({ items, locateAsync }) (未命中时它会去拉取,然后返回索引)。下面更完整的写法是给按别的字段寻址、或者需要完全掌控的场景准备的。

tsx
const photo = useOverlayUrlState({
  param: 'photo',
  codec: { decode: (raw) => raw, encode: (id) => id },
  locator: {
    locate: (id) => images.findIndex((x) => x.id === id),
    identify: (index) => images[index].id,
    locateAsync: async (id) => {
      const loaded = await loadById(id); // or loadUntil(id) — fetch just that one, or page up to it
      if (!loaded) return null; // exhausted — link names no image
      setImages(loaded); // commit — the overlay renders from this state
      return loaded.findIndex((x) => x.id === id); // wherever it landed
    },
  },
});

<LightboxUrlOverlay controller={photo} images={images} />
  • 怎么加载由你决定 —— 可以一页页拉到目标处,也可以只拉那一张图片再追加进去。URL 是按身份而不是按位置寻址的,所以 findIndex 返回它最终落在哪里都行。
  • 在 locateAsync 未完成期间,Lightbox保持关闭,参数也不动,因此深链能熬过这次请求。 null 或请求被拒绝则会移除参数。
  • 如果结果在 URL 已经变化、播放器已关闭或组件已卸载之后才到达,就会被丢弃 —— 慢请求不能打开一张没人要的幻灯片。
  • 等待期间什么都不渲染;加载状态本来就归页面自己管,所以请渲染你自己的骨架屏。
  • 没有超时机制 —— Lightbox无从得知画廊有多长。分页用尽时请以 null 结束,否则浮层会一直关着。
  • 无论 locateAsync 返回什么都以它为准 —— 就是它刚取到的数据的索引,直接采用,不会再去读一遍 images.

自动前进

设置 autoAdvance,Lightbox 打开时就会以幻灯片方式播放画廊。默认关闭,因此现有的 Lightbox 渲染和行为都与以前一样。

tsx
import { LightboxOverlay } from '@reelkit/react-lightbox';

<LightboxOverlay
  isOpen={index !== null}
  images={images}
  initialIndex={index ?? 0}
  onClose={() => setIndex(null)}
  autoAdvance
  autoAdvanceInterval={4000}
/>;
  • 图片 停留 autoAdvanceInterval(默认 5000 ms),从图片加载完成的那一刻开始计算,所以加载慢的图片绝不会在没人看到之前被跳过。加载失败的图片从出错时开始计算。
  • 视频 只播放一次而不循环,播放结束后前进。使用 useVideoSlideRenderer 时这些都已接好;自定义的 renderSlide 从 props 中读取 autoAdvance,并通过 onEnded 报告结束。
  • 它会等待:鼠标停在控件(按钮、链接或说明文字)上时、滑动过程中以及页面隐藏时,之后从停下的地方继续。
  • 手动切换(箭头、按键、滚轮、滑动)会让新幻灯片的间隔从零开始;幻灯片播放保持开启。
  • 在最后一张 时停止,除非设置了 loop。控件中的播放 / 暂停按钮显示当前状态,并让观看者切换它。

自定义的 renderControls 会收到 isAutoAdvancing,并且仅在 autoAdvance 开启时收到 onToggleAutoAdvance —— 有它时就渲染一个 PlayPauseButton。计时本身是 @reelkit/core 中的 createSlideshowController;想在你自己的播放器里实现同样的行为,见自动播放指南,它的方法见核心 API。

下载

设置 showDownload,即可在控件中添加下载按钮。它从当前项的 downloadUrl(没有时回退到 src)保存文件,文件名取 downloadName,没有时回退到 URL 的最后一段。

tsx
import { LightboxOverlay, type LightboxItem } from '@reelkit/react-lightbox';

const images: LightboxItem[] = [
  {
    src: 'https://cdn.example.com/beach-1600.jpg',
    // Save the original rather than the resized preview
    downloadUrl: 'https://cdn.example.com/beach-original.jpg',
    downloadName: 'beach.jpg',
  },
];

<LightboxOverlay
  isOpen={index !== null}
  images={images}
  initialIndex={index ?? 0}
  onClose={() => setIndex(null)}
  showDownload
/>;

文件会先被获取再保存,因为浏览器会忽略跨域链接上的 download 属性,转而直接打开文件。获取失败时 —— 通常是另一个源上的服务器没有发送 CORS 头 —— 文件会在新标签页中打开,观看者仍可在那里保存。为文件配置 Access-Control-Allow-Origin 即可一键保存。下载进行期间按钮处于禁用状态。

自定义的 renderControls 会收到 isDownloading,并且仅在 showDownload 开启时收到 onDownload —— 有它时就渲染一个 DownloadButton。工具函数是 @reelkit/core 中的 downloadFile;见下载指南。

API 参考

LightboxOverlay 属性

LightboxOverlayProps

属性类型默认值说明
isOpenboolean必填控制Lightbox的显示。如果希望由 URL 驱动打开状态,请改用独立的 LightboxUrlOverlay —— 见下面的 URL 状态。
imagesLightboxItem[]必填要显示的图片数组
ariaLabelstring'Image gallery'对话框区域的无障碍标签;Lightbox打开时由屏幕阅读器播报
initialIndexnumber0起始图片索引
transitionFnTransitionTransformFnslideTransition幻灯片过渡函数。可以引入内置的(slideTransition、flipTransition、lightboxFadeTransition、lightboxZoomTransition),也可以传自定义的。省略时默认为 slideTransition。
autoAdvancebooleanfalse以幻灯片方式播放画廊。见自动前进。
autoAdvanceIntervalnumber5000幻灯片播放期间每张图片停留的时长(毫秒),从图片加载完成后开始计算
showDownloadbooleanfalse为当前项添加下载按钮。见下载。
apiRefMutableRefObject<ReelApi>-用于访问 Reel API 的 ref
renderControls(props: ControlsRenderProps) => ReactNode-自定义控件,替换默认的关闭按钮、计数器和全屏开关
renderNavigation(props: NavigationRenderProps) => ReactNode-自定义导航,替换默认的上一张 / 下一张箭头
renderInfo(props: InfoRenderProps) => ReactNode-自定义信息浮层,替换默认的标题 + 描述渐变层。返回 null 则隐藏。
renderSlide(props: SlideRenderProps) => ReactNode | null-自定义幻灯片渲染。接收 { item, index, size, isActive, autoAdvance, onReady, onWaiting, onError, onEnded }。返回 null 则回退到默认实现。
renderLoading(props: { item: LightboxItem; activeIndex: number }) => ReactNode-自定义加载提示,替换默认的转圈动画
renderError(props: { item: LightboxItem; activeIndex: number }) => ReactNode-自定义错误提示,替换默认的错误图标

LightboxUrlOverlay 属性

LightboxUrlOverlayProps

接受上面所有视觉和行为属性,除了 isOpen,并把它换成 controller. initialIndex 在这里会被忽略 —— 由控制器的 position 决定打开哪一张,所以同时传入的值每次打开都会被覆盖。

属性类型默认值说明
controllerUrlStateController必填来自 useOverlayUrlState 的控制器。它的 position 决定浮层是否打开、显示哪一张;浮层会在切换幻灯片和关闭时通过它写回。

回调

属性类型说明
onClose() => voidLightbox关闭时调用。在 LightboxOverlay 上是必填的(打开状态归你管,所以关闭也得你处理);在 LightboxUrlOverlay 上是可选的,那里由 URL 驱动关闭 —— 只在你需要关闭后做点什么时才传。
onSlideChange(index: number) => void幻灯片切换后调用

Reel 属性(透传)

这些属性会转发给底层的 Reel 组件。它们的类型是 ReelProxyProps,即浮层透传的那部分 Reel 属性;用它为转发这些属性的包装组件标注类型。

属性类型默认值说明
loopbooleanfalse启用无限循环
enableNavKeysbooleantrue启用键盘导航
enableWheelbooleantrue启用鼠标滚轮导航
wheelDebounceMsnumber200滚轮防抖时长(毫秒)
transitionDurationnumber300过渡动画时长(毫秒)
swipeDistanceFactornumber0.12滑动阈值(0-1)
swipeToCloseDirection'up' | 'down''up'移动端滑动关闭手势的方向

类型

LightboxItem

typescript
interface LightboxItem {
  src: string;
  type?: 'image' | 'video';  // defaults to 'image'
  poster?: string;            // thumbnail for video items
  title?: string;
  description?: string;
  width?: number;
  height?: number;
  downloadUrl?: string;       // what the download button saves; defaults to src
  downloadName?: string;      // file name; defaults to the URL's last segment
}

ControlsRenderProps

typescript
interface ControlsRenderProps {
  item: LightboxItem;
  activeIndex: number;
  count: number;
  isFullscreen: boolean;
  isAutoAdvancing: boolean;
  isDownloading: boolean;
  onClose: () => void;
  onToggleFullscreen: () => void;
  onToggleAutoAdvance?: () => void; // only with autoAdvance
  onDownload?: () => void;          // only with showDownload
}
typescript
interface NavigationRenderProps {
  item: LightboxItem;
  activeIndex: number;
  count: number;
  onPrev: () => void;
  onNext: () => void;
}

SlideRenderProps

typescript
interface SlideRenderProps {
  item: LightboxItem;
  index: number;
  size: [number, number];
  isActive: boolean;
  autoAdvance: boolean; // a video should play once, not loop
  onReady: () => void;
  onWaiting: () => void;
  onError: () => void;
  onEnded: () => void;  // a video played to its end
}

InfoRenderProps

typescript
interface InfoRenderProps {
  item: LightboxItem;
  index: number;
}

子组件

可复用的子组件,用于通过 renderControls.

CloseButton

默认的 ✕ 关闭按钮。属性(CloseButtonProps):onClick,以及可选的 className 和 style。

tsx
import { CloseButton } from '@reelkit/react-lightbox';

<CloseButton onClick={onClose} />

计数器

显示 “1 / 3” 的图片计数标签。属性(CounterProps):currentIndex(从零开始)、count,以及可选的 className 和 style。

tsx
import { Counter } from '@reelkit/react-lightbox';

<Counter currentIndex={activeIndex} count={count} />

FullscreenButton

全屏开关按钮(Maximize/Minimize 图标)。属性(FullscreenButtonProps):isFullscreen、onToggle,以及可选的 className 和 style。

tsx
import { FullscreenButton } from '@reelkit/react-lightbox';

<FullscreenButton isFullscreen={isFullscreen} onToggle={onToggleFullscreen} />

SoundButton

视频幻灯片的静音开关按钮(Volume2/VolumeX 图标)。已自动包含在 renderControls from useVideoSlideRenderer中。若要在自定义控件里单独使用,请通过 useSoundState. 属性(SoundButtonProps):isMuted、onToggle,以及可选的 className 和 style。

tsx
import { SoundButton } from '@reelkit/react-lightbox';
import { useSoundState } from '@reelkit/react';

// Inside a component wrapped in SoundProvider:
function CustomControls({ onClose }) {
  const soundState = useSoundState();

  return (
    <div>
      <SoundButton
        isMuted={soundState.muted.value}
        onToggle={soundState.toggle}
      />
      <button onClick={onClose}>Close</button>
    </div>
  );
}

PlayPauseButton

幻灯片播放开关(Play/Pause 图标,播放时带 aria-pressed)。属性(PlayPauseButtonProps):isPlaying、onToggle,以及可选的 className 和 style。autoAdvance 开启时默认控件会显示它。

tsx
import {
  LightboxOverlay,
  PlayPauseButton,
  CloseButton,
} from '@reelkit/react-lightbox';

<LightboxOverlay
  autoAdvance
  renderControls={({ isAutoAdvancing, onToggleAutoAdvance, onClose }) => (
    <>
      {onToggleAutoAdvance && (
        <PlayPauseButton
          isPlaying={isAutoAdvancing}
          onToggle={onToggleAutoAdvance}
        />
      )}
      <CloseButton onClick={onClose} />
    </>
  )}
  {...props}
/>;

DownloadButton

下载按钮(Download 图标),isDownloading 时处于禁用状态。属性(DownloadButtonProps):isDownloading、onClick,以及可选的 className 和 style。showDownload 开启时默认控件会显示它。

tsx
import {
  LightboxOverlay,
  DownloadButton,
  CloseButton,
} from '@reelkit/react-lightbox';

<LightboxOverlay
  showDownload
  renderControls={({ isDownloading, onDownload, onClose }) => (
    <>
      {onDownload && (
        <DownloadButton isDownloading={isDownloading} onClick={onDownload} />
      )}
      <CloseButton onClick={onClose} />
    </>
  )}
  {...props}
/>;

Hooks

useVideoSlideRenderer

按需启用视频支持的 Hook。返回 renderSlide, renderControls、 SoundProvider —— 把浮层包在 SoundProvider 里,并把这些渲染函数传进去。返回类型是 UseVideoSlideRendererResult,其中还带有 hasVideo,任一条目为 type: 'video' 时为 true。

它的 renderSlide 用 LightboxVideoSlide 绘制视频条目:这是一个通过同一个共享 <video> 元素播放的视频幻灯片,因此在 iOS 上声音也能正常工作,并且会记住每个视频的播放位置。只有在自定义 renderSlide 时才需要自己渲染它。它的属性(LightboxVideoSlideProps)有 src、poster、isActive、size、slideKey(作为保存的位置和截取帧的键)、loop(默认 true),以及 onPlaying、onWaiting、onError、onEnded 回调。

typescript
import { useVideoSlideRenderer } from '@reelkit/react-lightbox';

const { renderSlide, renderControls, SoundProvider, hasVideo } =
  useVideoSlideRenderer(items);

// SoundProvider  — wrap LightboxOverlay in this for mute/unmute support
// renderSlide    — pass to LightboxOverlay's renderSlide prop
// renderControls — pass to LightboxOverlay's renderControls prop
//                  (includes Counter, FullscreenButton, SoundButton, CloseButton)
// hasVideo       — true if items contain at least one video

useFullscreen

管理全屏状态的 Hook,跨浏览器可用。

tsx
import { useRef } from 'react';
import { useFullscreen } from '@reelkit/react';

function CustomLightbox() {
  const containerRef = useRef<HTMLDivElement>(null);
  const [isFullscreen, requestFullscreen, exitFullscreen, toggleFullscreen] =
    useFullscreen({ ref: containerRef });

  return (
    <div ref={containerRef}>
      <button onClick={toggleFullscreen}>
        {isFullscreen.value ? 'Exit Fullscreen' : 'Enter Fullscreen'}
      </button>
    </div>
  );
}

过渡动画

把任意 TransitionTransformFn 通过 transitionFn 属性传入。只引入你用到的那个过渡,打包器就能把其余的摇掉。省略时默认为 slideTransition 。

函数来源说明
slideTransition@reelkit/react-lightbox标准横向滑动(默认)
lightboxFadeTransition@reelkit/react-lightbox图片之间交叉淡入淡出
flipTransition@reelkit/react-lightbox3D 翻卡效果
lightboxZoomTransition@reelkit/react-lightbox从缩小状态放大到正常尺寸
tsx
import {
  LightboxOverlay,
  lightboxFadeTransition,
} from '@reelkit/react-lightbox';

<LightboxOverlay
  isOpen={isOpen}
  images={images}
  initialIndex={0}
  onClose={handleClose}
  transitionFn={lightboxFadeTransition}
/>

自定义过渡函数

自己写一个 TransitionTransformFn ,通过 transitionFn属性传入。签名与核心滑动器的过渡函数一致。

tsx
import { LightboxOverlay } from '@reelkit/react-lightbox';
import type { TransitionTransformFn } from '@reelkit/react';

const customFade: TransitionTransformFn = (offset, size) => ({
  transform: `translate3d(${offset * size[0]}px, 0, 0)`,
  opacity: 1 - Math.min(Math.abs(offset), 1),
});

<LightboxOverlay
  isOpen={isOpen}
  images={images}
  transitionFn={customFade}
  onClose={() => setIsOpen(false)}
/>

CSS 类名

所有界面元素都使用普通 CSS 类名(不是 CSS Modules),可以在 @reelkit/react-lightbox/styles.css之后加载的样式表里用更高优先级的选择器覆盖它们。若只是改颜色、尺寸和 z-index,请优先使用下面 主题定制 一节。

类名组件说明
.rk-lightbox-overlayOverlay根容器(全屏背景层)
.rk-lightbox-spinnerOverlay默认的加载转圈动画
.rk-lightbox-img-errorOverlay错误状态容器(图片 / 视频损坏)
.rk-lightbox-img-error-textOverlay错误状态文字标签
.rk-lightbox-swipe-hintOverlay移动端滑动提示
.rk-lightbox-controls-left控制内容左上角控件容器
.rk-lightbox-btn控制内容控制按钮(全屏等)
.rk-lightbox-play-pause控制内容幻灯片播放 / 暂停按钮(与 .rk-lightbox-btn 一起使用)
.rk-lightbox-download控制内容下载按钮(与 .rk-lightbox-btn 一起使用)
.rk-lightbox-close控制内容关闭按钮
.rk-lightbox-counter控制内容图片计数标签
.rk-lightbox-nav导航导航箭头(两侧)
.rk-lightbox-nav-prev导航上一张箭头
.rk-lightbox-nav-next导航下一张箭头
.rk-lightbox-infoInfo标题 / 描述容器
.rk-lightbox-titleInfo图片标题
.rk-lightbox-descriptionInfo图片描述
.rk-lightbox-slideSlide幻灯片容器
.rk-lightbox-imgSlide图片元素
.rk-lightbox-video-containerVideoSlide视频幻灯片容器(按需开启)
.rk-lightbox-video-elementVideoSlide视频元素(按需开启)
.rk-lightbox-video-posterVideoSlide视频封面图(按需开启)

主题定制

每一个颜色、尺寸、z-index 和过渡都放在 CSS 自定义属性里。在 :root (或Lightbox的任意祖先元素)上覆盖,即可在不改组件源码的情况下换主题。

变量默认值控制内容
--rk-lightbox-overlay-bg#000全屏背景颜色
--rk-lightbox-overlay-z9999浮层 z-index
--rk-lightbox-top-shade-height80px顶部渐变遮罩高度
--rk-lightbox-top-shade-bglinear-gradient(rgba(0,0,0,0.6), transparent)顶部渐变遮罩颜色
--rk-lightbox-edge-padding16px关闭、导航和左上角控件距边缘的内缩
--rk-lightbox-controls-gap12px左上角控件之间的间距
--rk-lightbox-transition0.2s按钮悬停过渡时长
--rk-lightbox-blur8px按钮与标签的背景模糊半径
--rk-lightbox-btn-bgrgba(0, 0, 0, 0.5)关闭、导航和小按钮的默认背景
--rk-lightbox-btn-bg-hoverrgba(255, 255, 255, 0.2)关闭、导航和小按钮的悬停背景
--rk-lightbox-btn-fg#fff关闭、导航和小按钮的图标颜色
--rk-lightbox-btn-size36px小按钮尺寸(全屏切换等)
--rk-lightbox-btn-disabled-opacity0.5禁用状态下小按钮的不透明度(下载进行中)
--rk-lightbox-close-size40px关闭按钮尺寸
--rk-lightbox-nav-size48px上一个 / 下一个箭头尺寸
--rk-lightbox-nav-opacity0.7上一个 / 下一个箭头的空闲不透明度
--rk-lightbox-counter-fg#fff计数文字颜色
--rk-lightbox-counter-bgrgba(0, 0, 0, 0.5)计数标签背景
--rk-lightbox-counter-size14px计数字号
--rk-lightbox-counter-padding6px 12px计数标签内边距
--rk-lightbox-counter-radius20px计数标签圆角半径
--rk-lightbox-spinner-size28px加载指示器的默认宽高
--rk-lightbox-spinner-trackrgba(255, 255, 255, 0.2)加载指示器轨道颜色
--rk-lightbox-spinner-fg#fff加载指示器颜色
--rk-lightbox-spinner-duration0.8s加载指示器旋转时长
--rk-lightbox-error-fgrgba(255, 255, 255, 0.4)错误图标和文字颜色
--rk-lightbox-error-text-size13px错误信息字号
--rk-lightbox-info-bglinear-gradient(transparent, rgba(0,0,0,0.8))说明文字遮罩渐变
--rk-lightbox-info-padding24px说明文字内边距
--rk-lightbox-title-size18px标题字号
--rk-lightbox-description-size14px描述字号
--rk-lightbox-info-fg#fff说明文字颜色
--rk-lightbox-hint-fgrgba(255, 255, 255, 0.5)滑动提示文字颜色
--rk-lightbox-hint-bgrgba(0, 0, 0, 0.3)滑动提示标签背景
--rk-lightbox-hint-duration3s滑动提示淡入淡出的总时长
--rk-lightbox-video-bg#000<video> 后方的留黑背景

把下面这段放进在 @reelkit/react-lightbox/styles.css.

css
/* Brand the lightbox */
:root {
  --rk-lightbox-overlay-bg: #0f172a;
  --rk-lightbox-btn-bg: rgba(99, 102, 241, 0.65);
  --rk-lightbox-btn-bg-hover: rgba(168, 85, 247, 0.85);
  --rk-lightbox-nav-size: 56px;
  --rk-lightbox-counter-bg: rgba(99, 102, 241, 0.65);
  --rk-lightbox-info-bg: linear-gradient(
    transparent,
    rgba(99, 102, 241, 0.55) 60%,
    rgba(168, 85, 247, 0.85)
  );
}

无障碍

浮层根节点是一个模态对话框(role="dialog", aria-modal="true")。设置 ariaLabel 可以改变屏幕阅读器的播报内容,默认是 “Image gallery”。每张幻灯片都带有 role="group", aria-roledescription="slide"、 aria-label="第 N 张,共 M 张".

Lightbox打开时捕获焦点,关闭时把焦点还给触发元素。Tab 和 Shift+Tab 在内部的可聚焦元素之间循环;跑出去的焦点(点击外部、程序化聚焦)会被拉回来。实现基于 captureFocusForReturn 和 createFocusTrap from @reelkit/core.

键盘快捷键

Key作用
ArrowLeft上一张图片
ArrowRight下一张图片
Escape关闭 Lightbox(全屏时则退出全屏)