Stories Player

一个面向 React 的 Instagram 风格 Stories 播放浮层,基于 @reelkit/react-stories-player.

查看在线演示 →

特性

嵌套导航
点击切换 story,滑动切换分组
视频 story
自动播放,带声音开关
自动播放
每个 story 可配置计时
3D 过渡
立方体、翻转、淡入、缩放、滑动
进度条
基于 canvas 的分段进度
图片与视频
两种媒体类型都支持
虚拟化
DOM 里只有 3 张幻灯片
双击点赞
双击时的爱心动画
桌面端导航
桌面端的箭头按钮
story 圆环
Instagram 风格的头像圆环
泛型类型
用自定义数据扩展 StoryItem
Render Props
每个界面元素都可定制
URL 状态
可分享的 ?story=group.story 链接

安装

bash

别忘了引入样式:

typescript
图标
默认页眉使用 lucide-react 作为图标。如果你想换一套图标库,可以用 renderHeader renderNavigation 提供自己的实现。

快速上手

StoriesOverlay 组件渲染一个全屏 Stories Player。搭配 StoriesRingList 作为 Instagram 风格的入口。传入一组 StoriesGroup 对象,并用 isOpen.

tsx

在线演示

StoriesPlayer.tsx
Alice
Alice
Bob
Bob
Charlie
Charlie

点圆环打开播放器。点左右两侧切换 story,滑动切换用户。

URL 状态

StoriesUrlOverlay 是一个独立组件,它的打开状态存放在地址栏里。两条轴共用一个参数 —— ?story=<group>.<story> —— 于是正在播放的 story 就有了可分享、可收藏、能用返回键关闭的链接。用 useOverlayUrlState urlIndexTwoAxisKey构建控制器,再作为 controller.

内置的 key
Stories 是双轴的,所以请把双轴 key 展开进控制器: urlIndexTwoAxisKey (分组和 story 都按位置)或 urlStableIdTwoAxisKey (分组按稳定的 id)—— 两者都从 @reelkit/react. See the URL 状态指南 核心 API.
tsx
  • 打开时压入 一条 历史记录。滑动 story 切换用户都是 替换 它,因此导航 N 次也不会多出记录,退一步永远就是关闭播放器。返回键关闭播放器,不会逐个后退 story。
  • 内层导航也会被记录。 story 索引不会被冻结在分组粒度上 —— 在某个用户的 story 之间前进时会更新 ?story=2.n ,因此深链能精确落到那一个 story。
  • 只有在应用内部打开播放器时返回键才会关闭它 —— 因为那次链接压入了一条记录。在新标签页里直接打开的分享链接背后没有历史,浏览器返回会离开站点;这时用 ✕ 按钮或 Escape 就地移除参数并留在页面上。
  • 指向不存在的分组或 story 的参数 —— 过期的书签、手改的值、超出分组末尾的 story —— 会从 URL 中移除,而不是打开相邻的那个。

带路由的应用 —— 请传入适配器。 绕过路由器直接写 history.pushState 会让它的 location 过期,下一次导航就会把参数丢掉:

tsx

稳定的链接。 分组默认是按位置的,所以收藏下来的 ?story=2.0 在信息流重新排序后就会打开另一个用户。请改用稳定 id 来寻址分组 —— outerCodec 把 id 写进 URL, outerLocator 负责找到它在哪。story 那一半仍然是解析出的分组内的普通索引。

tsx

无限信息流。 翻页是 outerLocator 该管的事,与编解码器无关。 locate 是同步的,因此只能回答已经加载过的分组 —— 只加载了 20 个时,指向第 400 个分组的分享链接就查不到。 locateAsync 是兜底,只有在 locate 未命中时才调用;story 会以最终落到的那个分组重新校准上界。

同一个 locateAsync,作用在外层轴
这和单轴 key 接受的 locateAsync 翻页器是同一个 —— 在双轴 key 上它跟随你传入的 outerLocator ,因此分组这条轴负责翻页,而 story 仍是解析出的分组内的局部索引。
tsx
  • locateAsync 未完成期间,播放器保持关闭,参数也不动,因此深链能熬过这次请求。返回 null 或请求被拒绝则会移除参数。
  • 如果结果在 URL 已经变化、播放器已关闭或组件已卸载之后才到达,就会被丢弃 —— 慢请求不能打开一个没人要的 story。
  • 完整的 useOverlayUrlState 选项见 React API 参考,完整讲解见 React 指南.

API 参考

StoriesOverlayProps

StoriesOverlayProps<T>

属性类型默认值说明
isOpenboolean必填控制浮层的显示。为 true 时会锁住 body 滚动。
groupsStoriesGroup<T>[]必填要展示的 story 分组数组
onClose() => void必填关闭浮层的回调
ariaLabelstring'Stories player'对话框区域的无障碍标签;浮层打开时由屏幕阅读器播报
initialGroupIndexnumber0初始可见分组的索引(从 0 开始)
initialStoryIndexnumber0分组内初始可见 story 的索引(从 0 开始)
groupTransitionTransitionTransformFncubeTransition外层(分组)滑动器的过渡效果
defaultImageDurationnumber5000图片类 story 的默认自动播放时长(毫秒)
tapZoneSplitnumber0.3点击区域的分割比例(0–1)。左侧触发上一个,右侧触发下一个。
hideUIOnPausebooleantrue长按暂停时是否隐藏 story 界面(页眉、页脚)
enableKeyboardbooleantrue启用键盘导航(左右方向键、Escape)
innerTransitionDurationnumber200内层(story)过渡动画的时长(毫秒)
minSegmentWidthnumber8进度条分段的最小宽度(像素)
apiRefMutableRefObject<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 —— 它们改由控制器提供。

属性类型默认值说明
controllerUrlStateController<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) => voidFired when a story's timer completes
onDoubleTap(groupIndex: number, storyIndex: number) => void双击手势时触发
onPause() => void播放器暂停时触发
onResume() => void播放器恢复时触发

过渡动画

groupTransition 属性控制在用户分组之间滑动时的 3D 过渡效果。过渡函数请从 @reelkit/react:

tsx

内容加载生命周期

每个 story 幻灯片都通过 SlideRenderProps:

回调何时
onReady内容就绪(图片已加载、视频在播放)。进度计时开始。
onWaiting内容卡住(视频播放途中缓冲)。显示转圈动画并暂停计时。
onError内容加载失败。显示错误浮层。
onDurationReady上报媒体的真实时长(例如来自视频元数据),以便用正确的时长重启计时。
onEnded表示媒体已播完(例如视频结束)。会前进到下一个 story。
预加载缓存
内置的 ImageStorySlide VideoStorySlide 组件会在后台预加载下一个 story。用户切到已预加载的 story 时,内容会立刻出现,不会有加载动画。

Render Props

每个界面元素都可以通过 render props 替换。每个都会收到带类型的属性,包含所需的全部状态和回调。

renderHeader

替换默认页眉(作者信息、暂停 / 静音按钮、关闭按钮):

tsx

renderFooter

在 story 内容下方加一个页脚:

tsx

renderSlide

完全替换默认的图片 / 视频幻灯片。可以用 ImageStorySlide VideoStorySlide 这些子组件来复用内置的媒体处理:

tsx

renderNavigation

替换默认的桌面端箭头按钮:

tsx

renderProgressBar

用自定义实现替换默认的 canvas 进度条。 progress 信号发出 0 到 1 的值:

tsx

renderLoading

内容加载期间的自定义加载提示:

tsx

renderError

内容加载失败时的自定义错误浮层:

tsx

StoriesApi

Use the apiRef 属性做命令式控制:

tsx

方法

方法类型说明
nextStory()() => void在当前分组内前进到下一个 story
prevStory()() => void在当前分组内回到上一个 story
nextGroup()() => void切到下一个用户分组
prevGroup()() => void切到上一个用户分组
goToGroup(index)(index: number) => void按索引跳到指定分组
pause()() => void暂停自动播放和进度计时
resume()() => void恢复自动播放和进度计时

双击与点赞

双击时会播放内置的爱心动画,给出即时的视觉反馈。 onDoubleTap 回调会带上分组和 story 索引,你可以据此把点赞存进自己的状态里(调接口、本地存储等等)。播放器内部并不管理点赞状态。

tsx

定制爱心动画

通过 --rk-stories-heart-duration 变量调整动画速度(见 主题定制)。要改颜色、尺寸或者干脆隐藏爱心,请直接针对 .rk-stories-heart 类。 HeartAnimation 组件也单独导出,可以独立使用。

css
内置的爱心动画目前还不能通过 render prop 替换。你可以用 CSS 改样式,或者用 display: none 把它隐藏,然后在 onDoubleTap 回调里做自己的动画。如果你需要一个 renderDoubleTap render prop,欢迎通过 GitHub Issues.

子组件

对外导出的可复用积木,用于在自定义 render props 中组合:

CanvasProgressBar

基于 canvas 的高性能分段进度条。为每个 story 渲染一段,并用 requestAnimationFrame为当前段做填充动画。story 很多的分组会启用滑动窗口。

tsx

StoryHeader

默认页眉,包含作者头像、名称、认证徽章、相对时间、暂停 / 播放开关、静音开关、加载动画和关闭按钮。未提供 renderHeader 时自动使用。

tsx

ImageStorySlide

铺满的图片幻灯片,使用 object-fit: cover。通过回调上报加载 / 错误状态以便跟踪生命周期。

tsx

VideoStorySlide

视频幻灯片,使用共享的 <video> 元素以保证 iOS 上声音连续。它负责自动播放、封面帧、声音同步,并上报时长和播放生命周期事件。

tsx

StoriesRing

带 Instagram 风格渐变圆环的圆形头像。分段表示已读 / 未读 story —— 未读是渐变色,已读是灰色。

tsx

StoriesRingList

可横向滚动的一排 StoriesRing 组件,带作者名称。每个分组一个圆环。

tsx

HeartAnimation

双击触发的爱心动画浮层。在 800 毫秒内放大并淡出。可通过 CSS 定制(见“双击与点赞”一节)。

tsx

类型

StoryItem

typescript

AuthorInfo

typescript

StoriesGroup<T>

typescript

HeaderRenderProps<T>

typescript

FooterRenderProps<T>

typescript

SlideRenderProps<T>

typescript
typescript

ProgressBarRenderProps<T>

typescript

LoadingRenderProps<T>

typescript

ErrorRenderProps<T>

typescript

StoriesApi

typescript

自定义 Story 类型

扩展 StoryItem ,加上自定义字段,再把类型参数传给 StoriesOverlay。所有 render props 都会收到你扩展后的类型:

tsx

CSS 类名

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

类名组件说明
.rk-stories-overlayOverlay固定的全屏背景层(背景、z-index)
.rk-stories-swipe-wrapperOverlay滑动关闭的包装层(容纳导航按钮 + canvas)
.rk-stories-containerOverlay圆角的 story 画布(定位、溢出)
.rk-stories-ui-layerOverlay界面浮层容器(页眉、进度、导航)
.rk-stories-ui-layer--hiddenOverlay界面隐藏状态(由 hideUIOnPause 切换)
.rk-stories-errorOverlay错误状态(居中图标 + 文字)
.rk-stories-error-textOverlay错误信息文字
.rk-stories-nav-btn导航桌面端上一张 / 下一张箭头
.rk-stories-progress-barProgressBarcanvas 进度条的定位包装层
.rk-stories-slide-wrapperGroup一个 story 分组(外层幻灯片)
.rk-stories-storyStory单个 story(内层幻灯片根节点)
.rk-stories-headerStoryHeader页眉栏(头像、名称、操作)
.rk-stories-header--hiddenStoryHeader页眉隐藏状态(visible=false)
.rk-stories-header-avatarStoryHeader作者头像图片
.rk-stories-header-nameStoryHeader作者名称文字
.rk-stories-header-verifiedStoryHeader认证徽章容器
.rk-stories-header-timeStoryHeader相对时间文字
.rk-stories-header-actionsStoryHeader右侧操作(关闭、静音、暂停)
.rk-stories-header-btnStoryHeader页眉操作按钮
.rk-stories-header-spinnerStoryHeader视频缓冲转圈动画
.rk-stories-imageImageStorySlide图片 story 元素
.rk-stories-videoVideoStorySlide视频 story 容器
.rk-stories-video-elementVideoStorySlide共享的 <video> 元素
.rk-stories-video-posterVideoStorySlide视频封面图(播放时淡出)
.rk-stories-video-poster--visibleVideoStorySlide封面图可见状态(播放前)
.rk-stories-heartHeartAnimation双击时的爱心弹出动画
.rk-stories-ringStoriesRingstory 圆环(带动画渐变边框的头像)
.rk-stories-ring--activeStoriesRing含未读 story 的圆环(会动)
.rk-stories-ring-avatarStoriesRing圆环内部的头像图片
.rk-stories-ring-listStoriesRingList横向圆环列表容器
.rk-stories-ring-list-itemStoriesRingList圆环 + 名称的一列
.rk-stories-ring-list-nameStoriesRingList每个圆环下方的作者名称

主题定制

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

变量默认值控制内容
--rk-stories-overlay-bg#000Full-screen backdrop color
--rk-stories-overlay-z9999Overlay z-index
--rk-stories-container-radius12pxRounded corners on the story canvas (desktop)
--rk-stories-swipe-gap16pxGap between nav buttons and the story canvas
--rk-stories-top-shade-height120pxTop gradient scrim height behind the header
--rk-stories-top-shade-bglinear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%)Top gradient scrim color
--rk-stories-ui-transition200msFade duration when hideUIOnPause toggles
--rk-stories-nav-size44pxDesktop prev/next button size
--rk-stories-nav-bgrgba(255, 255, 255, 0.1)Desktop nav button background
--rk-stories-nav-bg-hoverrgba(255, 255, 255, 0.2)Desktop nav button hover background
--rk-stories-nav-fgrgba(255, 255, 255, 0.7)Desktop nav button icon color
--rk-stories-nav-fg-hover#fffDesktop nav button hover icon color
--rk-stories-error-bglinear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)Error state background gradient
--rk-stories-error-fgrgba(255, 255, 255, 0.5)Error icon and text color
--rk-stories-error-text-size13pxError message font size
--rk-stories-video-bg#000Letterbox background behind <video>
--rk-stories-video-poster-transition200msPoster fade duration when the video starts playing
--rk-stories-header-top18pxVertical offset of the header from the top of the story
--rk-stories-header-padding12px 16pxInner padding of the header row
--rk-stories-header-avatar-size32pxAvatar width/height
--rk-stories-header-name-fg#fffAuthor name color
--rk-stories-header-name-size14pxAuthor name font size
--rk-stories-header-time-fgrgba(255, 255, 255, 0.6)Time-ago text color
--rk-stories-header-btn-fg#fffHeader action icon color (close, mute, pause)
--rk-stories-heart-duration800msPop-in/fade-out animation duration
--rk-stories-ring-spin-duration4sActive ring gradient rotation duration
--rk-stories-ring-list-gap12pxSpacing between rings in the list
--rk-stories-ring-list-padding12pxInner padding around the ring list
--rk-stories-ring-list-name-size12pxAuthor name font size below each ring

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

css

无障碍

浮层根节点是一个模态对话框(role="dialog", aria-modal="true")。设置 ariaLabel 可以改变屏幕阅读器的播报内容,默认是 “Stories player”。

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

键盘快捷键

Key作用
ArrowLeftPrevious story
ArrowRightNext story
EscapeClose player