特性
安装
别忘了引入样式:
图标
lucide-react 作为图标。如果你想换一套图标库,可以用 renderHeader 和 renderNavigation 提供自己的实现。快速上手
StoriesOverlay 组件渲染一个全屏 Stories Player。搭配 StoriesRingList 作为 Instagram 风格的入口。传入一组 StoriesGroup 对象,并用 isOpen.
在线演示
点圆环打开播放器。点左右两侧切换 story,滑动切换用户。
URL 状态
StoriesUrlOverlay 是一个独立组件,它的打开状态存放在地址栏里。两条轴共用一个参数 —— ?story=<group>.<story> —— 于是正在播放的 story 就有了可分享、可收藏、能用返回键关闭的链接。用 useOverlayUrlState 和 urlIndexTwoAxisKey构建控制器,再作为 controller.
- 打开时压入 一条 历史记录。滑动 story 和 切换用户都是 替换 它,因此导航 N 次也不会多出记录,退一步永远就是关闭播放器。返回键关闭播放器,不会逐个后退 story。
- 内层导航也会被记录。 story 索引不会被冻结在分组粒度上 —— 在某个用户的 story 之间前进时会更新
?story=2.n,因此深链能精确落到那一个 story。 - 只有在应用内部打开播放器时返回键才会关闭它 —— 因为那次链接压入了一条记录。在新标签页里直接打开的分享链接背后没有历史,浏览器返回会离开站点;这时用 ✕ 按钮或 Escape 就地移除参数并留在页面上。
- 指向不存在的分组或 story 的参数 —— 过期的书签、手改的值、超出分组末尾的 story —— 会从 URL 中移除,而不是打开相邻的那个。
带路由的应用 —— 请传入适配器。 绕过路由器直接写 history.pushState 会让它的 location 过期,下一次导航就会把参数丢掉:
稳定的链接。 分组默认是按位置的,所以收藏下来的 ?story=2.0 在信息流重新排序后就会打开另一个用户。请改用稳定 id 来寻址分组 —— outerCodec 把 id 写进 URL, outerLocator 负责找到它在哪。story 那一半仍然是解析出的分组内的普通索引。
无限信息流。 翻页是 outerLocator 该管的事,与编解码器无关。 locate 是同步的,因此只能回答已经加载过的分组 —— 只加载了 20 个时,指向第 400 个分组的分享链接就查不到。 locateAsync 是兜底,只有在 locate 未命中时才调用;story 会以最终落到的那个分组重新校准上界。
同一个 locateAsync,作用在外层轴
locateAsync 翻页器是同一个 —— 在双轴 key 上它跟随你传入的 outerLocator ,因此分组这条轴负责翻页,而 story 仍是解析出的分组内的局部索引。- 在
locateAsync未完成期间,播放器保持关闭,参数也不动,因此深链能熬过这次请求。返回null或请求被拒绝则会移除参数。 - 如果结果在 URL 已经变化、播放器已关闭或组件已卸载之后才到达,就会被丢弃 —— 慢请求不能打开一个没人要的 story。
- 完整的
useOverlayUrlState选项见 React API 参考,完整讲解见 React 指南.
API 参考
StoriesOverlayProps
StoriesOverlayProps<T>
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| isOpen | boolean | 必填 | 控制浮层的显示。为 true 时会锁住 body 滚动。 |
| groups | StoriesGroup<T>[] | 必填 | 要展示的 story 分组数组 |
| onClose | () => void | 必填 | 关闭浮层的回调 |
| ariaLabel | string | 'Stories player' | 对话框区域的无障碍标签;浮层打开时由屏幕阅读器播报 |
| initialGroupIndex | number | 0 | 初始可见分组的索引(从 0 开始) |
| initialStoryIndex | number | 0 | 分组内初始可见 story 的索引(从 0 开始) |
| groupTransition | TransitionTransformFn | cubeTransition | 外层(分组)滑动器的过渡效果 |
| defaultImageDuration | number | 5000 | 图片类 story 的默认自动播放时长(毫秒) |
| tapZoneSplit | number | 0.3 | 点击区域的分割比例(0–1)。左侧触发上一个,右侧触发下一个。 |
| hideUIOnPause | boolean | true | 长按暂停时是否隐藏 story 界面(页眉、页脚) |
| enableKeyboard | boolean | true | 启用键盘导航(左右方向键、Escape) |
| innerTransitionDuration | number | 200 | 内层(story)过渡动画的时长(毫秒) |
| minSegmentWidth | number | 8 | 进度条分段的最小宽度(像素) |
| apiRef | MutableRefObject<StoriesApi | null> | - | 用于访问命令式 StoriesApi 的 ref |
| renderHeader | (props: HeaderRenderProps<T>) => ReactNode | - | 自定义页眉渲染器。接收作者、story 以及暂停 / 静音状态。 |
| renderFooter | (props: FooterRenderProps<T>) => ReactNode | - | 自定义页脚渲染器。接收作者和 story 信息。 |
| renderSlide | (props: SlideRenderProps<T>) => ReactNode | - | 自定义幻灯片渲染器,替换默认的图片 / 视频幻灯片。 |
| renderNavigation | (props: NavigationRenderProps) => ReactNode | - | 自定义桌面端导航。替换默认的上一张 / 下一张箭头按钮。 |
| renderProgressBar | (props: ProgressBarRenderProps<T>) => ReactNode | - | 自定义进度条。替换默认的 canvas 进度条。 |
| renderLoading | (props: LoadingRenderProps<T>) => ReactNode | - | 自定义加载界面渲染器。不提供时显示默认的页眉转圈动画。 |
| renderError | (props: ErrorRenderProps<T>) => ReactNode | - | 自定义错误界面渲染器。不提供时显示默认的错误图标浮层。 |
StoriesUrlOverlayProps
StoriesUrlOverlayProps<T>
接受 StoriesOverlay 的所有属性,除了打开状态那三个 —— isOpen, initialGroupIndex, initialStoryIndex —— 它们改由控制器提供。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| controller | UrlStateController<TwoAxisPosition> | 必填 | 来自 useOverlayUrlState 并展开了 urlIndexTwoAxisKey 的控制器。它的 position —— 一个 { outer, inner } 对象 —— 决定播放器是否打开、打开到哪里;浮层会在每次导航和关闭时写回。 |
回调
| 属性 | 类型 | 说明 |
|---|---|---|
| onClose | () => void | 播放器关闭时调用。在 StoriesOverlay 上是必填的(打开状态归你管,所以关闭也得你处理);在 StoriesUrlOverlay 上是可选的,那里由 URL 驱动关闭 —— 只在你需要关闭后做点什么时才传。 |
| onStoryChange | (groupIndex: number, storyIndex: number) => void | 当前 story 变化时触发 |
| onGroupChange | (groupIndex: number) => void | 当前分组变化时触发 |
| onStoryViewed | (groupIndex: number, storyIndex: number) => void | 某个 story 变为可见时触发 |
| onStoryComplete | (groupIndex: number, storyIndex: number) => void | Fired when a story's timer completes |
| onDoubleTap | (groupIndex: number, storyIndex: number) => void | 双击手势时触发 |
| onPause | () => void | 播放器暂停时触发 |
| onResume | () => void | 播放器恢复时触发 |
过渡动画
groupTransition 属性控制在用户分组之间滑动时的 3D 过渡效果。过渡函数请从 @reelkit/react:
内容加载生命周期
每个 story 幻灯片都通过 SlideRenderProps:
| 回调 | 何时 |
|---|---|
| onReady | 内容就绪(图片已加载、视频在播放)。进度计时开始。 |
| onWaiting | 内容卡住(视频播放途中缓冲)。显示转圈动画并暂停计时。 |
| onError | 内容加载失败。显示错误浮层。 |
| onDurationReady | 上报媒体的真实时长(例如来自视频元数据),以便用正确的时长重启计时。 |
| onEnded | 表示媒体已播完(例如视频结束)。会前进到下一个 story。 |
预加载缓存
ImageStorySlide 和 VideoStorySlide 组件会在后台预加载下一个 story。用户切到已预加载的 story 时,内容会立刻出现,不会有加载动画。Render Props
每个界面元素都可以通过 render props 替换。每个都会收到带类型的属性,包含所需的全部状态和回调。
renderHeader
替换默认页眉(作者信息、暂停 / 静音按钮、关闭按钮):
renderFooter
在 story 内容下方加一个页脚:
renderSlide
完全替换默认的图片 / 视频幻灯片。可以用 ImageStorySlide 和 VideoStorySlide 这些子组件来复用内置的媒体处理:
renderNavigation
替换默认的桌面端箭头按钮:
renderProgressBar
用自定义实现替换默认的 canvas 进度条。 progress 信号发出 0 到 1 的值:
renderLoading
内容加载期间的自定义加载提 示:
renderError
内容加载失败时的自定义错误浮层:
StoriesApi
Use the apiRef 属性做命令式控 制:
方法
| 方法 | 类型 | 说明 |
|---|---|---|
| nextStory() | () => void | 在当前分组内前进到下一个 story |
| prevStory() | () => void | 在当前分组内回到上一个 story |
| nextGroup() | () => void | 切到下一个用户分组 |
| prevGroup() | () => void | 切到上一个用户分组 |
| goToGroup(index) | (index: number) => void | 按索引跳到指定分组 |
| pause() | () => void | 暂停自动播放和进度计时 |
| resume() | () => void | 恢复自动播放和进度计时 |
双击与点赞
双击时会播放内置的爱心动画,给出即时的视觉反馈。 onDoubleTap 回调会带上分组和 story 索引,你可以据此把点赞存进自己的状态里(调接口、本地存储等等)。播放器内部并不管理点赞状态。
定制爱心动画
通过 --rk-stories-heart-duration 变量调整动画速度(见 主题定制)。要改颜色、尺寸或者干脆隐藏爱心,请直接针对 .rk-stories-heart 类。 HeartAnimation 组件也单独导出,可以独立使用。
display: none 把它隐藏,然后在 onDoubleTap 回调里做自己的动画。如果你需要一个 renderDoubleTap render prop,欢迎通过 GitHub Issues.子组件
对外导出的可复用积木,用于在自定义 render props 中组合:
CanvasProgressBar
基于 canvas 的高性能分段进度条。为每个 story 渲染一段,并用 requestAnimationFrame为当前段做填充动画。story 很多的分组会启用滑动窗口。
StoryHeader
默认页眉,包含作者头像、名称、认证徽章、相对时间、暂停 / 播放开关、静音开关、加载动画和关闭按钮。未提供 renderHeader 时自动使用。
ImageStorySlide
铺满的图片幻灯片,使用 object-fit: cover。通过回调上报加载 / 错误状态以便跟踪生命周期。
VideoStorySlide
视频幻灯片,使用共享的 <video> 元素以保证 iOS 上声音连续。它负责自动播放、封面帧、声音同步,并上报时长和播放生命周期事件。
StoriesRing
带 Instagram 风格渐变圆环的圆形头像。分段表示已读 / 未读 story —— 未读是渐变色,已读是灰色。
StoriesRingList
可横向滚动的一排 StoriesRing 组件,带作者名称。每个分组一个圆环。
HeartAnimation
双击触发的爱心动画浮层。在 800 毫秒内放大并淡出。可通过 CSS 定制(见“双击与点赞”一节)。
类型
StoryItem
AuthorInfo
StoriesGroup<T>
HeaderRenderProps<T>
FooterRenderProps<T>
SlideRenderProps<T>
NavigationRenderProps
ProgressBarRenderProps<T>
LoadingRenderProps<T>
ErrorRenderProps<T>
StoriesApi
自定义 Story 类型
扩展 StoryItem ,加上自定义字段,再把类型参数传给 StoriesOverlay。所有 render props 都会收到你扩展后的类型:
CSS 类名
所有 CSS 类名都是普通类名(不是 CSS Modules),因此可以在 @reelkit/react-stories-player/styles.css之后加载的样式表里用更高优先级的选择器覆盖它们。若只是改颜色、尺寸和 z-index,请优先使用下面 主题定制 一节。
| 类名 | 组件 | 说明 |
|---|---|---|
| .rk-stories-overlay | Overlay | 固定的全屏背景层(背景、z-index) |
| .rk-stories-swipe-wrapper | Overlay | 滑动关闭的包装层(容纳导航按钮 + canvas) |
| .rk-stories-container | Overlay | 圆角的 story 画布(定位、溢出) |
| .rk-stories-ui-layer | Overlay | 界面浮层容器(页眉、进度、导航) |
| .rk-stories-ui-layer--hidden | Overlay | 界面隐藏状态(由 hideUIOnPause 切换) |
| .rk-stories-error | Overlay | 错误状态(居中图标 + 文字) |
| .rk-stories-error-text | Overlay | 错误信息文字 |
| .rk-stories-nav-btn | 导航 | 桌面端上一张 / 下一张箭头 |
| .rk-stories-progress-bar | ProgressBar | canvas 进度条的定位包装层 |
| .rk-stories-slide-wrapper | Group | 一个 story 分组(外层幻灯片) |
| .rk-stories-story | Story | 单个 story(内层幻灯片根节点) |
| .rk-stories-header | StoryHeader | 页眉栏(头像、名称、操作) |
| .rk-stories-header--hidden | StoryHeader | 页眉隐藏状态(visible=false) |
| .rk-stories-header-avatar | StoryHeader | 作者头像图片 |
| .rk-stories-header-name | StoryHeader | 作者名称文字 |
| .rk-stories-header-verified | StoryHeader | 认证徽章容器 |
| .rk-stories-header-time | StoryHeader | 相对时间文字 |
| .rk-stories-header-actions | StoryHeader | 右侧操作(关闭、静音、暂停) |
| .rk-stories-header-btn | StoryHeader | 页眉操作按钮 |
| .rk-stories-header-spinner | StoryHeader | 视频缓冲转圈动画 |
| .rk-stories-image | ImageStorySlide | 图片 story 元素 |
| .rk-stories-video | VideoStorySlide | 视频 story 容器 |
| .rk-stories-video-element | VideoStorySlide | 共享的 <video> 元素 |
| .rk-stories-video-poster | VideoStorySlide | 视频封面图(播放时淡出) |
| .rk-stories-video-poster--visible | VideoStorySlide | 封面图可见状态(播放前) |
| .rk-stories-heart | HeartAnimation | 双击时的爱心弹出动画 |
| .rk-stories-ring | StoriesRing | story 圆环(带动画渐变边框的头像) |
| .rk-stories-ring--active | StoriesRing | 含未读 story 的圆环(会动) |
| .rk-stories-ring-avatar | StoriesRing | 圆环内部的头像图片 |
| .rk-stories-ring-list | StoriesRingList | 横向圆环列表容器 |
| .rk-stories-ring-list-item | StoriesRingList | 圆环 + 名称的一列 |
| .rk-stories-ring-list-name | StoriesRingList | 每个圆环下方的作者名称 |
主题定制
每一个颜色、尺寸、z-index 和过渡都放在 CSS 自定义属性里。在 :root (或浮层的任意祖先元素)上覆盖其中一个或多个,即可在不改组件源码的情况下换主题。
| 变量 | 默认值 | 控制内容 |
|---|---|---|
| --rk-stories-overlay-bg | #000 | Full-screen backdrop color |
| --rk-stories-overlay-z | 9999 | Overlay z-index |
| --rk-stories-container-radius | 12px | Rounded corners on the story canvas (desktop) |
| --rk-stories-swipe-gap | 16px | Gap between nav buttons and the story canvas |
| --rk-stories-top-shade-height | 120px | Top gradient scrim height behind the header |
| --rk-stories-top-shade-bg | linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%) | Top gradient scrim color |
| --rk-stories-ui-transition | 200ms | Fade duration when hideUIOnPause toggles |
| --rk-stories-nav-size | 44px | Desktop prev/next button size |
| --rk-stories-nav-bg | rgba(255, 255, 255, 0.1) | Desktop nav button background |
| --rk-stories-nav-bg-hover | rgba(255, 255, 255, 0.2) | Desktop nav button hover background |
| --rk-stories-nav-fg | rgba(255, 255, 255, 0.7) | Desktop nav button icon color |
| --rk-stories-nav-fg-hover | #fff | Desktop nav button hover icon color |
| --rk-stories-error-bg | linear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%) | Error state background gradient |
| --rk-stories-error-fg | rgba(255, 255, 255, 0.5) | Error icon and text color |
| --rk-stories-error-text-size | 13px | Error message font size |
| --rk-stories-video-bg | #000 | Letterbox background behind <video> |
| --rk-stories-video-poster-transition | 200ms | Poster fade duration when the video starts playing |
| --rk-stories-header-top | 18px | Vertical offset of the header from the top of the story |
| --rk-stories-header-padding | 12px 16px | Inner padding of the header row |
| --rk-stories-header-avatar-size | 32px | Avatar width/height |
| --rk-stories-header-name-fg | #fff | Author name color |
| --rk-stories-header-name-size | 14px | Author name font size |
| --rk-stories-header-time-fg | rgba(255, 255, 255, 0.6) | Time-ago text color |
| --rk-stories-header-btn-fg | #fff | Header action icon color (close, mute, pause) |
| --rk-stories-heart-duration | 800ms | Pop-in/fade-out animation duration |
| --rk-stories-ring-spin-duration | 4s | Active ring gradient rotation duration |
| --rk-stories-ring-list-gap | 12px | Spacing between rings in the list |
| --rk-stories-ring-list-padding | 12px | Inner padding around the ring list |
| --rk-stories-ring-list-name-size | 12px | Author name font size below each ring |
把下面这段放进在 @reelkit/react-stories-player/styles.css.
无障碍
浮层根节点是一个模态对话框(role="dialog", aria-modal="true")。设置 ariaLabel 可以改变屏幕阅读器的播报内容,默认是 “Stories player”。
浮层 打开时捕获焦点,关闭时把焦点还给触发元素。Tab 和 Shift+Tab 在内部的可聚焦元素之间循环;跑出去的焦点(点击外部、程序化聚焦)会被拉回来。实现基于 captureFocusForReturn 和 createFocusTrap from @reelkit/core.
键盘快捷键
| Key | 作用 |
|---|---|
| ArrowLeft | Previous story |
| ArrowRight | Next story |
| Escape | Close player |