Stories Player
@reelkit/react-stories-player を使った、React 用の Instagram 風のストーリーのオーバーレイです。
機能
インストール
npm i @reelkit/react-stories-player @reelkit/react lucide-reactスタイルのインポートを忘れないでください:
import '@reelkit/react-stories-player/styles.css';アイコン
デフォルトのヘッダーはアイコンに lucide-react を使います。別のアイコンライブラリを使いたい場合は、renderHeader と renderNavigation で独自のものを渡してください。
クイックスタート
StoriesOverlay コンポーネントはフルスクリーンのストーリーのプレイヤーを描画します。Instagram 風の入り口として StoriesRingList と組み合わせてください。StoriesGroup オブジェクトの配列を渡し、isOpen で表示を制御します。
import { useState } from 'react';
import {
StoriesOverlay,
StoriesRingList,
createStoriesViewedStateController,
type StoriesGroup,
} from '@reelkit/react-stories-player';
import '@reelkit/react-stories-player/styles.css';
const groups: StoriesGroup[] = [
{
author: {
id: 'user-1',
name: 'Alice',
avatar: '/cdn/samples/avatars/avatar-06.jpg',
verified: true,
},
stories: [
{
id: 's1-1',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-001.jpg',
},
{
id: 's1-2',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-002.jpg',
},
{
id: 's1-3',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-003.jpg',
},
],
},
{
author: {
id: 'user-2',
name: 'Bob',
avatar: '/cdn/samples/avatars/avatar-07.jpg',
},
stories: [
{
id: 's2-1',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-004.jpg',
},
{
id: 's2-2',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-005.jpg',
},
],
},
{
author: {
id: 'user-3',
name: 'Charlie',
avatar: '/cdn/samples/avatars/avatar-08.jpg',
verified: true,
},
stories: [
{
id: 's3-1',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-006.jpg',
},
],
},
];
export default function App() {
const [isOpen, setIsOpen] = useState(false);
const [selectedGroup, setSelectedGroup] = useState(0);
// What was seen: rings, resume and recording from one controller, kept in
// localStorage across reloads.
const [viewed] = useState(() =>
createStoriesViewedStateController({
storageKey: 'stories-seen',
groups: () => groups,
}),
);
const openStories = (groupIndex: number) => {
setSelectedGroup(groupIndex);
setIsOpen(true);
};
return (
<div style={{ padding: 16, background: '#0f172a', minHeight: '100vh' }}>
<StoriesRingList groups={groups} viewed={viewed} onSelect={openStories} />
<StoriesOverlay
isOpen={isOpen}
onClose={() => setIsOpen(false)}
groups={groups}
initialGroupIndex={selectedGroup}
viewed={viewed}
/>
</div>
);
}import { useState } from 'react';
import {
StoriesOverlay,
StoriesRingList,
createStoriesViewedStateController,
type StoriesGroup,
} from '@reelkit/react-stories-player';
import '@reelkit/react-stories-player/styles.css';
const groups: StoriesGroup[] = [
{
author: {
id: 'user-1',
name: 'Alice',
avatar: '/cdn/samples/avatars/avatar-06.jpg',
verified: true,
},
stories: [
{
id: 's1-1',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-001.jpg',
},
{
id: 's1-2',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-002.jpg',
},
{
id: 's1-3',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-003.jpg',
},
],
},
{
author: {
id: 'user-2',
name: 'Bob',
avatar: '/cdn/samples/avatars/avatar-07.jpg',
},
stories: [
{
id: 's2-1',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-004.jpg',
},
{
id: 's2-2',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-005.jpg',
},
],
},
{
author: {
id: 'user-3',
name: 'Charlie',
avatar: '/cdn/samples/avatars/avatar-08.jpg',
verified: true,
},
stories: [
{
id: 's3-1',
mediaType: 'image',
src: '/cdn/samples/images/stories/story-006.jpg',
},
],
},
];
export default function App() {
const [isOpen, setIsOpen] = useState(false);
const [selectedGroup, setSelectedGroup] = useState(0);
// What was seen: rings, resume and recording from one controller, kept in
// localStorage across reloads.
const [viewed] = useState(() =>
createStoriesViewedStateController({
storageKey: 'stories-seen',
groups: () => groups,
}),
);
const openStories = (groupIndex: number) => {
setSelectedGroup(groupIndex);
setIsOpen(true);
};
return (
<div style={{ padding: 16, background: '#0f172a', minHeight: '100vh' }}>
<StoriesRingList groups={groups} viewed={viewed} onSelect={openStories} />
<StoriesOverlay
isOpen={isOpen}
onClose={() => setIsOpen(false)}
groups={groups}
initialGroupIndex={selectedGroup}
viewed={viewed}
/>
</div>
);
}再読み込み後も視聴済みを覚えているリングの列はストーリーズトレイのレシピを参照してください。
URL の状態
ライブデモを見る →StoriesUrlOverlay は、開閉状態をアドレスバーに持つ別のコンポーネントです。2 つの軸が 1 つのパラメーター(?story=<group>.<story>)に乗るので、再生中のストーリーに、共有でき、ブックマークでき、戻るボタンで閉じられるリンクが付きます。useOverlayUrlState と urlIndexTwoAxisKey でコントローラーを作り、controller として渡します。
組み込みのキー
ストーリーは二軸なので、二軸のキーをコントローラーに展開します。urlIndexTwoAxisKey(グループとストーリーを位置で指す)か urlStableIdTwoAxisKey(グループを安定した id で指す)で、どちらも @reelkit/react から再エクスポートされています。URL の状態のガイド と Core API を参照してください。
import {
StoriesUrlOverlay,
useOverlayUrlState,
urlIndexTwoAxisKey,
} from '@reelkit/react-stories-player';
import { Link } from 'react-router-dom';
const stories = useOverlayUrlState({
param: 'story',
...urlIndexTwoAxisKey({
outerCount: () => groups.length,
innerCounts: () => groups.map((g) => g.stories.length),
}),
});
// Opening a user is a link — the overlay reads the URL and opens itself.
{groups.map((g, i) => (
<Link key={g.author.id} to={`?story=${i}.0`}>{g.author.name}</Link>
))}
<StoriesUrlOverlay controller={stories} groups={groups} />- 開くと履歴エントリーを 1 つ 積みます。ストーリーのスワイプ も ユーザーの切り替えもそれを 置き換え るので、N 回移動してもエントリーは増えず、1 回戻れば必ずプレイヤーが閉じます。戻るとプレイヤーが閉じ、ストーリーは戻りません。
- 内側の移動も運ばれます。 ストーリーのインデックスはグループ単位で固定されず、ユーザーのストーリー内で進むと
?story=2.n が更新されるので、ディープリンクは正確なストーリーに着きます。 - 戻るで閉じるのは、アプリ内から開いた場合だけです(リンクがエントリーを積んだ場合)。新しいタブで直接開いた共有リンクには前の履歴がないので、ブラウザーの戻るではサイトを離れます。✕ ボタンか Escape なら、その場でパラメーターを取り除いてページにとどまります。
- どのグループやストーリーも指さないパラメーター(古いブックマーク、手で書き換えた値、グループの末尾を超えたストーリー)は、隣を開くのではなく URL から取り除かれます。
ルーターを使うアプリでは、アダプターを渡します。 ルーターの裏で history.pushState を書くと、その位置情報が古くなり、次のナビゲーションでパラメーターが消えます:
import { useReactRouterUrlAdapter } from '@reelkit/react/react-router-url-adapter';
const adapter = useReactRouterUrlAdapter();
const stories = useOverlayUrlState({
param: 'story',
adapter,
...urlIndexTwoAxisKey({
outerCount: () => groups.length,
innerCounts: () => groups.map((g) => g.stories.length),
}),
});
<StoriesUrlOverlay controller={stories} groups={groups} />安定したリンク。 グループはデフォルトで位置なので、ブックマークした ?story=2.0 はフィードが並べ替えられると別のユーザーを開きます。代わりにグループを安定した id で指してください。outerCodec が id を URL に書き、outerLocator がその場所を探します。innerCodec を innerLocate と innerIdentify と一緒に渡すと、グループ内のストーリーにも同じことができます。省略すると、ストーリーの側は単純なローカルのインデックスのままです。
const stories = useOverlayUrlState({
param: 'story',
...urlIndexTwoAxisKey({
outerCount: () => groups.length,
innerCounts: () => groups.map((g) => g.stories.length),
// ?story=user_42.3
outerCodec: { decode: (raw) => raw, encode: (id) => id },
outerLocator: {
locate: (id) => groups.findIndex((g) => g.author.id === id),
identify: (index) => groups[index].author.id,
},
// Optional: ?story=user_42.story_7 — the story by id too
innerCodec: { decode: (raw) => raw, encode: (id) => id },
innerLocate: (outer, id) => {
const index = groups[outer].stories.findIndex((s) => s.id === id);
return index === -1 ? null : index;
},
innerIdentify: (outer, index) => groups[outer].stories[index].id,
}),
});無限フィード。 ページングは codec とは関係なく、outerLocator の役目です。locate は同期なので、読み込み済みのグループにしか答えません。20 件を読み込んだフィードの 400 番目のグループへの共有リンクは見つかりません。locateAsync はフォールバックで、locate が見つけられなかったときだけ呼ばれます。ストーリーは、最終的に決まったグループの範囲に合わせて改めて制限されます。
同じ locateAsync を外側の軸で
これは一軸のキーが受け取るのと同じ locateAsync のページャーです。二軸のキーでは渡した outerLocator に乗るので、グループの軸がページングされ、ストーリーは解決されたグループ内のローカルなインデックスのままです。
const stories = useOverlayUrlState({
param: 'story',
...urlIndexTwoAxisKey({
outerCount: () => groups.length,
innerCounts: () => groups.map((g) => g.stories.length),
outerLocator: {
locate: (index) => (index < groups.length ? index : null),
identify: (index) => index,
locateAsync: async (index) => {
const loaded = await loadUntilGroup(index); // page up to it
if (!loaded) return null; // exhausted — link names no group
setGroups(loaded); // commit — the overlay renders from this state
return index;
},
},
}),
});locateAsyncが保留中のあいだ、プレイヤーは閉じたままで、パラメーターはそのまま残るので、ディープリンクは取得のあいだも失われません。nullか reject でパラメーターは取り除かれます。- URL が変わったあと、閉じたあと、アンマウントしたあとに届いた答えは破棄されます。遅い取得が、誰も求めていないストーリーを開くことはありません。
useOverlayUrlStateのすべてのオプションは React API リファレンス に、手順は React ガイド にあります。
見たところを覚えておく
リングは、グループを最後まで見るまでグラデーションを表示し、グループは閲覧者がまだ見ていない最初のストーリーから開きます。どちらも 1 つの createStoriesViewedStateController によるものです。フィードを持つ場所で一度だけ作成し、viewed としてリング一覧とプレイヤーに渡すと、ストアの読み取り、追従、表示したものの記録をコンポーネント側が行います。保存されたエントリーは作成者とストーリーを id で記録するため、フィードが並べ替えられても残り、同じコントローラーが Vue と Angular でも使えます。
import { useRef, useState } from 'react';
import {
StoriesOverlay,
StoriesRingList,
createStoriesViewedStateController,
type StoriesGroup,
} from '@reelkit/react-stories-player';
function Feed({ groups }: { groups: StoriesGroup[] }) {
const [open, setOpen] = useState(false);
const [group, setGroup] = useState(0);
// The controller reads the groups through a getter every time it needs
// them, so keep the getter current: a ref for a plain prop, or a signal.
const groupsRef = useRef(groups);
groupsRef.current = groups;
const [viewed] = useState(() =>
createStoriesViewedStateController({
storageKey: 'stories-seen',
groups: () => groupsRef.current,
}),
);
return (
<>
{/* The rings follow the controller by themselves. */}
<StoriesRingList
groups={groups}
viewed={viewed}
onSelect={(index) => {
setGroup(index);
setOpen(true);
}}
/>
{/* The player resumes, records and draws its card rings from it. */}
<StoriesOverlay
isOpen={open}
onClose={() => setOpen(false)}
groups={groups}
initialGroupIndex={group}
viewed={viewed}
/>
</>
);
}- 最初のストーリーも数えます。 画面にすでに表示されているストーリーはマウント時に既読として報告されるので、ストーリーが 1 つだけのグループは開いて閉じれば見たことになります。
- スワイプで進んでも続きから再開します。
resumeStoryIndexは、このセッションで初めて到達したすべてのグループ(プレイヤーが開いたグループも含む)で参照されます。ただしinitialStoryIndexがストーリーを直接指定している場合は別です。すでにスワイプで通過したグループは、離れたところから再び開きます。 - リンクが優先されます。
StoriesUrlOverlayでは、何が保存されていても、プレイヤーがどこで開くかはパラメーターが決めます。それ以外では再開のコールバックが決めます。 - 件数は合計ではなく位置です。 エントリーが表すのは到達したいちばん先のストーリーなので、見終えたグループにストーリーを追加するとリングがまた点灯し、途中から削除すると件数が減り ます。共有リンクと同じ自己修復です。
- ストレージは差し替えられます。 タブを閉じたら忘れるには
storage: createSessionStorageAdapter()を、ほかには独自のStorageAdapterを渡してください。開いている 2 つのタブは、ブラウザーの storage イベントで同期します。 - 開く瞬間にだけマウントされるプレイヤーは、読み取りが遅すぎます。 プレイヤーは、まだ閉じている間に動くエフェクトでストアを読み取ります。
{open && <StoriesOverlay … />}ではその機会がないので、コントローラーを持つコンポーネントでuseAttachViewedState(viewed)を呼んでください。マウント時に接続し、アンマウント時に手放します。オプションと、土台になっている低レベルの部品は Stories Core のページにあります。
デスクトップのカルーセル
desktopLayout="carousel" を指定すると、プレーヤーが Instagram のウェブ版のようなレイアウトになります。アクティブなストーリーはいつもの大きさのまま中央に置かれ、その左右に隣のグループが最大 2 つずつ、小さく暗めのカードとして並びます。カードをクリックするとそのグループが開き、グループが切り替わる間カードが横へスライドします。リング一覧に渡すのと同じ viewed コントローラーを渡すと、見終えたグループのカードは控えめなリングになります。カードが自分でそれに追従するため、ストーリーが既読になったときに再描画されるのはカードだけで、プレーヤーは再レンダリングされません。
// The same controller the ring list takes: the cards draw the same rings and
// repaint by themselves when a story is marked seen.
<StoriesOverlay
isOpen={open}
onClose={() => setOpen(false)}
groups={groups}
initialGroupIndex={group}
desktopLayout="carousel"
viewed={viewed}
/>;- スマートフォンでは通常のプレーヤーのままです。 幅 768px までならカードは描画されず、スワイプにはデフォルトの
'single'レイアウトと同じくgroupTransitionが使われます。ウィンドウの幅がこの境目をまたぐと、その場でレイアウトが切り替わります。 - カードにはグループが開く位置が表示されます:このセッションで離れたときのストーリー、なければ
resumeStoryIndexが返すストーリーです。動画のポスターか画像そのものを表示します。どちらもないストーリー(グラデーション上のテキストのストーリーなど)は、renderSlideがプレーヤーの大きさで描き、それを縮小して表示します。ポスターのない動画ではカードの背景が見えます。このプレビューはrenderSlideをisActive: falseで呼び出す、ただの画像です。中の要素にフォーカスもクリックも届きません。再生やアナリティクスなど、スライドがマウント時に行う処理はisActiveのときだけ実行してください。 - タイマーはスライドを待ちます。 カードの動きが止まるまで、ストーリーは進まず、そのストーリーの
onStoryViewedも呼ばれません。 - グループを切り替えるどの方法でもスライドします:カードのクリック、
StoriesApi.goToGroup、nextGroupとprevGroup、そしてグループの最後を越える矢印キーです。タッチのスワイプはプレーヤー自体を動かすので、その上にスライドは重なりません。
グループごとの進捗バーとヘッダー
デフォルトでは、進捗バーとヘッダーが 1 つずつプレーヤーの上にあり、グループが 切り替わった後で新しいグループの表示に変わります。chromePlacement="group" を指定すると、各グループが自分のスライドの中に専用の進捗バーとヘッダーを持ち、Instagram と同じようにグループと一緒に回転します。
// Every group carries its own progress bar and header, so both turn with the
// group instead of switching above the player once it has changed.
<StoriesOverlay
isOpen={open}
onClose={() => setOpen(false)}
groups={groups}
chromePlacement="group"
/>;- 隣のグループは、どこまで見たかを表示します。 プレーヤーがそのグループへ回転している間、進捗バーとヘッダーは、そのグループが開くストーリーを、まだ何も再生していない状態で表示します。
- 離れていくグループはそのままの状態を保ちます。 回転が終わるまで進捗バーはそれまでの進み具合を保つので、最後まで見たグループは回転の途中で空にならず、満たされたままです。
- 独自のレンダラーは画面上のグループそれぞれに対して呼ばれます。
renderProgressBarとrenderHeaderはgroupIndexとisActiveを受け取ります。隣のグループではisActiveがfalseになり、進捗のシグナルは変化しません。 - 独自のヘッダーはスワイプ領域の中にあります。 ヘッダーをタップするとストーリーが切り替わります。ただし
button、リンク、role="button"を持つ要素の上なら切り替わらないので、ヘッダー内の操作要素はすべてそのいずれかにしてください。 - デスクトップのカルーセルでは、スライド中の見た目は変わりません。 カードがスライドしている間はプレーヤーが隠れるため、この設定はグループが開いてから反映されます。
API リファレンス
StoriesOverlayProps
StoriesOverlayProps<T>
| Prop | 型 | デフォルト | 説明 |
|---|---|---|---|
isOpen | boolean | 必須 | オーバーレイの表示を制御します。true のとき body のスクロールが止まります。 |
groups | StoriesGroup<T>[] | 必須 | 表示するストーリーのグループの配列 |
onClose | () => void | 必須 | オーバーレイを閉じるコールバック |
ariaLabel | string | 'Stories player' | ダイアログ領域のアクセシブルなラベル。オーバーレイが開くとスクリーンリーダーが読み上げます |
initialGroupIndex | number | 0 | 最初に表示するグループの 0 始まりのインデックス |
initialStoryIndex | number | resumeStoryIndex(initialGroupIndex)、なければ 0 | グループ内で最初に表示するストーリーの 0 始まりのインデックス。指定すると覚えている位置より優先されます。省略すると、開いたグループもほかのグループと同じように続きから始まります。 |
resumeStoryIndex | (groupIndex: number) => number | — | グループに初めて到達したときに開くストーリー。閲覧者が前回の続きから見られます。今回開いてからまだ訪れていないグループでだけ参照されます。 |
groupTransition | TransitionTransformFn | cubeTransition | 外側(グループ)のスライダーのトランジション効果 |
defaultImageDuration | number | 5000 | 画像のストーリーを自動で送るまでのデフォルトの時間(ミリ秒) |
tapZoneSplit | number | 0.3 | タップゾーンの分割比(0–1)。左の部分で前へ、右で次へ移動します。 |
hideUIOnPause | boolean | true | 長押しによる一時停止の間、プログレスバーとヘッダーを隠します |
enableKeyboard | boolean | true | キーボード操作(左右の矢印、Escape)を有効にします |
innerTransitionDuration | number | 200 | 内側(ストーリー)のトランジションのアニメーションの長さ(ミリ秒) |
minSegmentWidth | number | 8 | 進捗バーのセグメントの最小幅(ピクセル) |
apiRef | MutableRefObject<StoriesApi | null> | - | 命令的な StoriesApi にアクセスするための ref |
desktopLayout | 'single' | 'carousel' | 'single' | デスクトップでのレイアウト。'carousel' ではアクティブなストーリーの横に隣のグループをプレビューカードで表示し、グループ間をスライドで切り替えます。スマートフォンでは常にストーリーだけを表示します。型は DesktopLayout として公開されています。 |
chromePlacement | 'overlay' | 'group' | 'overlay' | 進捗バーとヘッダーを置く場所。'overlay' はプレーヤーの上に 1 つ描き、'group' はグループのスライドごとに 1 つ描くので、両方がグループと一緒に回転します。型は ChromePlacement として公開されています。 |
viewed | StoriesViewedStateController | - | 閲覧者が見たもの(createStoriesViewedStateController() から)。グループは最初の未読ストーリーから再開し、表示したストーリーはすべて記録され、カルーセルのカードは見終えたグループのリングを控えめにします。同じコントローラーを StoriesRingList にも渡してください。 |
renderHeader | (props: HeaderRenderProps<T>) => ReactNode | - | 独自のヘッダーレンダラー。作者、ストーリー、一時停止とミュートの状態を受け取ります。chromePlacement="group" ではヘッダーがスワイプ領域の中に入ります。グループごとの進捗バーとヘッダーを参照してください。 |
renderFooter | (props: FooterRenderProps<T>) => ReactNode | - | 独自のフッターレンダラー。作者とストーリーの情報を受け取ります。 |
renderSlide | (props: SlideRenderProps<T>) => ReactNode | - | 独自のスライドレンダラー。デフォルトの画像と動画のスライドを置き換えます。 |
renderNavigation | (props: NavigationRenderProps) => ReactNode | - | デスクトップ用の独自のナビゲーション。デフォルトの前後の矢印ボタンを置き換えます。 |
renderProgressBar | (props: ProgressBarRenderProps<T>) => ReactNode | - | 独自の進捗バー。デフォルトの canvas の進捗バーを置き換えます。 |
renderLoading | (props: LoadingRenderProps<T>) => ReactNode | - | 独自の読み込み UI のレンダラー。指定しなければヘッダーのデフォルトのスピナーを表示します。 |
renderError | (props: ErrorRenderProps<T>) => ReactNode | - | 独自のエラー UI のレンダラー。指定しなければデフォルトのエラーアイコンのオーバーレイを表示します。 |
renderGroupPreview | (props: GroupPreviewRenderProps<T>) => ReactNode | - | デスクトップのカルーセル用の独自のカード。デフォルトのカードの中身を置き換えます。 |
StoriesUrlOverlayProps
StoriesUrlOverlayProps<T>
StoriesOverlay の props をすべて受け取りますが、開閉状態の 3 つ(isOpen、initialGroupIndex、initialStoryIndex)は代わりにコントローラーから与えられます。
| Prop | 型 | デフォルト | 説明 |
|---|---|---|---|
controller | UrlStateController<TwoAxisPosition> | 必須 | urlIndexTwoAxisKey を展開した useOverlayUrlState のコントローラー。その位置({ outer, inner } のオブジェクト)で、プレイヤーが開いているかどうかとどこで開くかが決まります。オーバーレイは移動のたびと閉じるときに書き戻します。 |
コールバック
| Prop | 型 | 説明 |
|---|---|---|
onClose | () => void | プレイヤーが閉じたときに呼ばれます。StoriesOverlay では必須です(開閉状態を持つのはあなたなので、閉じる処理も必要です)。StoriesUrlOverlay では任意で、閉じるのは URL が行うので、閉じたあとに反応したいときだけ渡します。 |
onStoryChange | (groupIndex: number, storyIndex: number) => void | アクティブなストーリーが変わったときに呼ばれます |
onGroupChange | (groupIndex: number) => void | アクティブなグループが変わったときに呼ばれます |
onStoryViewed | (groupIndex: number, storyIndex: number) => void | ストーリーが表示されたときに呼ばれます |
onStoryComplete | (groupIndex: number, storyIndex: number) => void | ストーリーのタイマーが終わったときに呼ばれます |
onDoubleTap | (groupIndex: number, storyIndex: number) => void | ダブルタップのジェスチャーで呼ばれます |
onPause | () => void | プレイヤーが一時停止したときに呼ばれます |
onResume | () => void | プレイヤーが再開したときに呼ばれます |
トランジション
groupTransition プロップは、ユーザーのグループ間をスワイプするときの 3D トランジション効果を決めます。トランジション関数は @reelkit/react からインポートします:
import {
cubeTransition, // default — 3D cube rotation
flipTransition, // card flip
fadeTransition, // crossfade
zoomTransition, // zoom in/out
slideTransition, // horizontal slide
} from '@reelkit/react';
<StoriesOverlay
isOpen={isOpen}
onClose={handleClose}
groups={groups}
groupTransition={flipTransition}
/>コンテンツ読み込みのライフサイクル
各ストーリーのスライドは、SlideRenderProps を通じて渡されるコールバックで読み込み状態を伝えます:
| コールバック | タイミング |
|---|---|
onReady | コンテンツの準備ができた(画像が読み込まれた、動画が再生中)。進捗のタイマーが始まります。 |
onWaiting | コンテンツが止まった(再生の途中で動画がバッファリング中)。スピナーが表示され、タイマーが止まります。 |
onError | コンテンツの読み込みに失敗した。エラーのオーバーレイが表示されます。 |
onDurationReady | メディアの実際の長さ(たとえば動画のメタデータから)を報告し、正しい長さでタイマーを再開します。 |
onEnded | メディアが終わった(たとえば動画が最後まで再生された)ことを伝えます。次のストーリーへ進みます。 |
プリローダーのキャッシュ
組み込みの ImageStorySlide と VideoStorySlide コンポーネントは、次のストーリーをバックグラウンドで先読みします。先読み済みのストーリーへ移動すると、読み込みのスピナーなしで内容がすぐに表示されます。
レンダープロップ
UI 要素はすべてレンダープロップで置き換えられます。それぞれが、必要な状態とコールバックをすべて持つ型付きの props を受け取ります。
renderHeader
デフォルトのヘッダー(作者の情報、一時停止とミュートのボタン、閉じるボタン)を置き換えます:
<StoriesOverlay
isOpen={isOpen}
onClose={handleClose}
groups={groups}
renderHeader={({ author, story, isPaused, isMuted, isVideo, onToggleSound, onTogglePause, onClose }) => (
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
padding: 16,
display: 'flex',
alignItems: 'center',
gap: 8,
zIndex: 10,
}}>
<img src={author.avatar} style={{ width: 32, height: 32, borderRadius: '50%' }} />
<span style={{ color: '#fff', fontWeight: 600 }}>{author.name}</span>
{isVideo && <button onClick={onToggleSound}>{isMuted ? 'Unmute' : 'Mute'}</button>}
<button onClick={onClose} style={{ marginLeft: 'auto' }}>Close</button>
</div>
)}
/>renderFooter
ストーリーの内容の下にフッターを追加します:
<StoriesOverlay
isOpen={isOpen}
onClose={handleClose}
groups={groups}
renderFooter={({ author, story, storyIndex }) => (
<div style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: 16,
background: 'linear-gradient(transparent, rgba(0,0,0,0.6))',
color: '#fff',
zIndex: 10,
}}>
<span>{author.name} — Story {storyIndex + 1}</span>
</div>
)}
/>renderSlide
デフォルトの画像と動画のスライドを完全に置き換えます。組み込みのメディア処理には ImageStorySlide と VideoStorySlide のサブコンポーネントを使ってください:
import {
StoriesOverlay,
ImageStorySlide,
VideoStorySlide,
} from '@reelkit/react-stories-player';
<StoriesOverlay
isOpen={isOpen}
onClose={handleClose}
groups={groups}
renderSlide={({ story, index, groupIndex, size, activeGroupIndex, activeStoryIndex, onDurationReady, onReady, onWaiting, onError, onEnded }) => {
const [w, h] = size;
if (story.mediaType === 'video') {
return (
<div style={{ width: w, height: h, background: '#000' }}>
<VideoStorySlide
src={story.src}
poster={story.poster}
groupIndex={groupIndex}
storyIndex={index}
activeGroupIndex={activeGroupIndex}
activeStoryIndex={activeStoryIndex}
onDurationReady={onDurationReady}
onPlaying={onReady}
onWaiting={onWaiting}
onEnded={onEnded}
onError={onError}
/>
</div>
);
}
return (
<div style={{ width: w, height: h, background: '#000' }}>
<ImageStorySlide src={story.src} onLoad={onReady} onError={onError} />
</div>
);
}}
/>renderNavigation
デスクトップのデフォルトの矢印ボタンを置き換えます:
<StoriesOverlay
isOpen={isOpen}
onClose={handleClose}
groups={groups}
renderNavigation={({ onPrevStory, onNextStory, onPrevGroup, onNextGroup }) => (
<div style={{ position: 'absolute', bottom: 16, left: 0, right: 0, display: 'flex', justifyContent: 'center', gap: 8, zIndex: 10 }}>
<button onClick={onPrevGroup}>Prev Group</button>
<button onClick={onPrevStory}>Prev</button>
<button onClick={onNextStory}>Next</button>
<button onClick={onNextGroup}>Next Group</button>
</div>
)}
/>renderProgressBar
デフォルトの canvas の進捗バーを独自の実装に置き換えます。progress シグナルは 0 から 1 の値を出します:
import { Observe } from '@reelkit/react';
<StoriesOverlay
isOpen={isOpen}
onClose={handleClose}
groups={groups}
renderProgressBar={({ totalStories, activeIndex, progress }) => (
<div style={{ display: 'flex', gap: 4, padding: '8px 16px' }}>
{Array.from({ length: totalStories }, (_, i) => (
<div key={i} style={{ flex: 1, height: 2, background: 'rgba(255,255,255,0.3)', borderRadius: 1, overflow: 'hidden' }}>
<Observe signals={[activeIndex, progress]}>
{() => {
const fill = i < activeIndex.value ? 1 : i === activeIndex.value ? progress.value : 0;
return <div style={{ width: `${fill * 100}%`, height: '100%', background: '#fff' }} />;
}}
</Observe>
</div>
))}
</div>
)}
/>renderLoading
コンテンツの取得中に表示する独自の読み込みインジケーター:
<StoriesOverlay
isOpen={isOpen}
onClose={handleClose}
groups={groups}
renderLoading={({ story, storyIndex, groupIndex }) => (
<div style={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
}}>
<span>Loading story {storyIndex + 1}...</span>
</div>
)}
/>renderError
コンテンツの読み込みに失敗したときの独自のエラーオーバーレイ:
<StoriesOverlay
isOpen={isOpen}
onClose={handleClose}
groups={groups}
renderError={({ story, storyIndex, groupIndex }) => (
<div style={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
color: '#fff',
background: 'rgba(0,0,0,0.8)',
}}>
<span style={{ fontSize: 48 }}>!</span>
<span>Failed to load story</span>
</div>
)}
/>renderGroupPreview
デスクトップのカルーセルのカードの中身を置き換えます。カードの配置、拡大縮小、スライドは引き続きプレーヤーが担当します。グループを開くには onOpen を呼びます:
<StoriesOverlay
isOpen={isOpen}
onClose={handleClose}
groups={groups}
desktopLayout="carousel"
renderGroupPreview={({ group, story, onOpen }) => (
<button
type="button"
className="my-story-card"
aria-label={`Open stories by ${group.author.name}`}
onClick={onOpen}
>
{story?.poster && <img src={story.poster} alt="" />}
<span>{group.author.name}</span>
</button>
)}
/>StoriesApi
命令的に制御するには apiRef プロップを使います:
import { useRef } from 'react';
import { StoriesOverlay, type StoriesApi } from '@reelkit/react-stories-player';
function App() {
const apiRef = useRef<StoriesApi | null>(null);
return (
<>
<button onClick={() => apiRef.current?.nextStory()}>Next Story</button>
<button onClick={() => apiRef.current?.nextGroup()}>Next Group</button>
<button onClick={() => apiRef.current?.pause()}>Pause</button>
<StoriesOverlay
isOpen={isOpen}
onClose={handleClose}
groups={groups}
apiRef={apiRef}
/>
</>
);
}メソッド
| メソッド | 型 | 説明 |
|---|---|---|
nextStory() | () => void | 現在のグループ内で次のストーリーへ進みます |
prevStory() | () => void | 現在のグループ内で前のストーリーへ戻ります |
nextGroup() | () => void | 次のユーザーのグループへ切り替えます |
prevGroup() | () => void | 前のユーザーのグループへ切り替えます |
goToGroup(index) | (index: number) => void | インデックスで指定したグループへ移動します |
pause() | () => void | 自動送りと進捗のタイマーを一時停止します |
resume() | () => void | 自動送りと進捗のタイマーを再開します |
ダブルタップといいね
ダブルタップすると組み込みのハートのアニメーションが再生され、すぐに視覚的なフィードバックが返ります。onDoubleTap コールバックはグループとストーリーのインデックス付きで呼ばれるので、いいねを自分の状態(API 呼び出し、ローカルストレージなど)に保存できます。プレイヤー自体は、いいねの状態を内部で管理しません。
<StoriesOverlay
isOpen={isOpen}
onClose={() => setIsOpen(false)}
groups={groups}
onDoubleTap={(groupIndex, storyIndex) => {
// Built-in heart animation plays automatically.
// Handle the like in your own state:
const story = groups[groupIndex].stories[storyIndex];
toggleLike(story.id);
}}
/>ハートのアニメーションをカスタマイズ する
アニメーションの速さは --rk-stories-heart-duration トークンで調整します(テーマ設定 を参照)。色、大きさ、またはハートを完全に隠すには、.rk-stories-heart クラスを直接指定してください。HeartAnimation コンポーネントも単独で使えるよう公開されています。
/* Speed up the pop via the token */
:root {
--rk-stories-heart-duration: 1s;
}
/* Restyle color and size via the class */
.rk-stories-heart {
color: #ff3b5c;
font-size: 80px;
}
/* Or hide the built-in heart entirely */
.rk-stories-heart {
display: none;
}組み込みのハートのアニメーションは、今のところレンダープロップでは置き換えられません。CSS でスタイルを変えるか、display: none で隠して onDoubleTap コールバックの中で独自のアニメーションを扱ってください。renderDoubleTap のレンダープロップが必要なら、GitHub Issues で知らせてください。
サブコンポーネント
独自のレンダープロップで組み合わせるために公開されている、再利用できる部品です。それぞれの props の型も一緒に公開されているので(CanvasProgressBarProps、StoryHeaderProps、ImageStorySlideProps、VideoStorySlideProps、StoriesRingProps、StoriesRingListProps、HeartAnimationProps)、部品を包むコンポーネントで使えます:
CanvasProgressBar
高性能な、canvas で描く分割された進捗バーです。ストーリーごとにセグメントを描き、アクティブなセグメントの塗りを requestAnimationFrame でアニメーションします。ストーリーの多いグループではスライドするウィンドウに対応します。グループがどこまで進んだかを示すだけのバーには live={false} を渡してください。アニメーションのループを持たず、シグナルが変わったときとサイズが変わった後にだけ描画します。
import { CanvasProgressBar } from '@reelkit/react-stories-player';
<CanvasProgressBar
totalStories={group.stories.length}
activeIndex={activeIndexSignal}
progress={progressSignal}
minSegmentWidth={8}
gap={2}
barHeight={2}
// A bar that is not live draws only when its signals change, with no
// animation loop: enough for a group that is not playing.
live={isActiveGroup}
/>StoryHeader
作者のアバター、名前、認証バッジ、相対的な時刻、一時停止と再生の切り替え、ミュートの切り替え、読み込みスピナー、閉じるボタンを持つデフォルトのヘッダーです。renderHeader を指定しない場合に自動で使われます。
import { StoryHeader } from '@reelkit/react-stories-player';
<StoryHeader
author={{ id: '1', name: 'Alice', avatar: '/avatar.jpg', verified: true }}
createdAt={new Date(Date.now() - 3600_000)}
onClose={handleClose}
isPaused={false}
onTogglePause={togglePause}
isMuted={true}
onToggleSound={toggleSound}
isVideo={true}
isLoading={false}
/>ImageStorySlide
object-fit: cover の、端まで広がる画像スライドです。ライフサイクルの追跡のために、読み込みとエラーをコールバックで報告します。
import { ImageStorySlide } from '@reelkit/react-stories-player';
<ImageStorySlide
src="/photo.jpg"
aspectRatio={9 / 16}
onLoad={() => console.log('loaded')}
onError={() => console.log('failed')}
/>VideoStorySlide
iOS で音声を途切れさせないために共有の <video> 要素を使う動画スライドです。自動再生、ポスターのフレーム、サウンドの同期を処理し、長さと再生のライフサイクルのイベントを報 告します。
import { VideoStorySlide } from '@reelkit/react-stories-player';
<VideoStorySlide
src="/clip.mp4"
poster="/clip-poster.jpg"
groupIndex={0}
storyIndex={2}
activeGroupIndex={activeGroupSignal}
activeStoryIndex={activeStorySignal}
onDurationReady={(ms) => console.log('duration:', ms)}
onPlaying={() => console.log('playing')}
onWaiting={() => console.log('buffering')}
onEnded={() => console.log('ended')}
onError={() => console.log('error')}
/>StoriesRing
Instagram 風のグラデーションのリングが付いた円形のアバターです。状態は 2 つで、まだ見るストーリーが残っているグループは回転するグラデーションに、すべて見たグループは落ち着いた平坦なリングになります。
import { StoriesRing } from '@reelkit/react-stories-player';
<StoriesRing
author={{ id: '1', name: 'Alice', avatar: '/avatar.jpg' }}
totalStories={5}
viewedCount={2}
onClick={() => openStories(0)}
/>StoriesRingList
作者の名前付きの StoriesRing コンポーネントが横に並ぶ、スクロールできる行です。グループごとに 1 つのリングです。
import { StoriesRingList } from '@reelkit/react-stories-player';
// `viewed` is the same createStoriesViewedStateController the player takes; the
// rings follow it by themselves.
<StoriesRingList
groups={groups}
viewed={viewed}
onSelect={(groupIndex) => openStories(groupIndex)}
/>;HeartAnimation
ダブルタップで表示されるハートのアニメーションのオーバーレイです。800ms かけて拡大しながらフェードアウトします。CSS でカスタマイズしてください(ダブルタップといいねの節を参照)。
import { HeartAnimation } from '@reelkit/react-stories-player';
<HeartAnimation onComplete={() => console.log('animation done')} />再エクスポート
利用者に必要なものはすべてこのパッケージから取得できるので、@reelkit/core を直接インポートする必要はありません。
| 元のパッケージ | 再エクスポートされるもの |
|---|---|
@reelkit/react | SoundProvider、useSoundState、useOverlayUrlState、createViewedStateController、twoAxisViewedTracking、createLocalStorageAdapter、createSessionStorageAdapter、createMemoryStorageAdapter、urlIndexTwoAxisKey、urlStableIdTwoAxisKey、base64UrlCodec、および型 UrlAdapter、UrlCodec、UrlLocator、UrlKey、UrlStateController、TwoAxisPosition、TwoAxisIdentity、UrlIndexTwoAxisKeyOptions、OverlayUrlStateOptions、ViewedStateController、ViewedStateOptions、StorageAdapter |
@reelkit/stories-core | createStoriesViewedStateController、StoriesViewedStateController、StoriesViewedStateControllerConfig、およびコンテンツの型 StoryItem、AuthorInfo、StoriesGroup、MediaType |
SoundProvider と useSoundState は、プレイヤーがスライドと共有するサウンドの状態を運びます。自分でメディアを再生するカスタムスライドは、useSoundState を通してミュートの切り替えを読み取ります。プレイヤーの中ではすでにプロバイダーが置かれています。それ以外の場所で描画する VideoStorySlide やカスタムスライドには、上に SoundProvider が必要で、ないと useSoundState が例外を投げます。
型
StoryItem
interface StoryItem {
id: string;
mediaType: 'image' | 'video';
src: string;
poster?: string;
duration?: number; // ms, images default to 5000, videos use natural duration
createdAt?: string | Date;
aspectRatio?: number; // width / height
}AuthorInfo
interface AuthorInfo {
id: string;
name: string;
avatar: string;
verified?: boolean;
}StoriesGroup<T>
interface StoriesGroup<T extends StoryItem = StoryItem> {
author: AuthorInfo;
stories: T[];
}HeaderRenderProps<T>
interface HeaderRenderProps<T extends StoryItem = StoryItem> {
author: AuthorInfo;
story: T;
storyIndex: number;
isPaused: boolean;
isMuted: boolean;
isVideo: boolean;
groupIndex: number;
isActive: boolean; // false for a neighbouring group's header
onToggleSound: () => void;
onTogglePause: () => void;
onClose: () => void;
}FooterRenderProps<T>
interface FooterRenderProps<T extends StoryItem = StoryItem> {
author: AuthorInfo;
story: T;
storyIndex: number;
}SlideRenderProps<T>
interface SlideRenderProps<T extends StoryItem = StoryItem> {
story: T;
index: number;
groupIndex: number;
isActive: boolean;
size: [number, number];
activeGroupIndex: Signal<number>;
activeStoryIndex: Signal<number>;
onDurationReady: (durationMs: number) => void;
onReady: () => void;
onWaiting: () => void;
onError: () => void;
onEnded: () => void;
}NavigationRenderProps
interface NavigationRenderProps {
onPrevStory: () => void;
onNextStory: () => void;
onPrevGroup: () => void;
onNextGroup: () => void;
}ProgressBarRenderProps<T>
interface ProgressBarRenderProps<T extends StoryItem = StoryItem> {
totalStories: number;
activeIndex: Signal<number>;
progress: Signal<number>;
group: StoriesGroup<T>;
groupIndex: number;
isActive: boolean; // false: the signals hold still for a neighbouring group
}LoadingRenderProps<T>
interface LoadingRenderProps<T extends StoryItem = StoryItem> {
story: T;
storyIndex: number;
groupIndex: number;
}ErrorRenderProps<T>
interface ErrorRenderProps<T extends StoryItem = StoryItem> {
story: T;
storyIndex: number;
groupIndex: number;
}GroupPreviewRenderProps<T>
type DesktopLayout = 'single' | 'carousel';
interface GroupPreviewRenderProps<T extends StoryItem = StoryItem> {
group: StoriesGroup<T>;
groupIndex: number;
story: T | undefined; // the story the group would open on
offset: number; // negative on the left, positive on the right
viewedCount: number; // from viewedState, 0 without it
onOpen: () => void;
}StoriesApi
interface StoriesApi {
nextStory: () => void;
prevStory: () => void;
nextGroup: () => void;
prevGroup: () => void;
goToGroup: (index: number) => void;
pause: () => void;
resume: () => void;
}カスタム Story 型
StoryItem を独自のフィールドで拡張し、型パラメーターを StoriesOverlay に渡します。すべてのレンダープロップが拡張した型を受け取ります:
import {
StoriesOverlay,
ImageStorySlide,
type StoryItem,
type StoriesGroup,
type SlideRenderProps,
} from '@reelkit/react-stories-player';
interface PromoStory extends StoryItem {
title: string;
subtitle?: string;
bgGradient?: string;
ctaText?: string;
}
const groups: StoriesGroup<PromoStory>[] = [
{
author: { id: 'brand', name: 'My Brand', avatar: '/brand.png', verified: true },
stories: [
{
id: 'promo-1',
mediaType: 'image',
src: '/sale-banner.jpg',
title: 'Flash Sale',
subtitle: 'Up to 50% off',
ctaText: 'Shop Now',
},
{
id: 'promo-2',
mediaType: 'image',
src: '',
title: 'Thank You',
subtitle: '10K followers!',
bgGradient: 'linear-gradient(135deg, #a18cd1, #fbc2eb)',
},
],
},
];
function CustomSlide({ story, size, onReady, onError }: SlideRenderProps<PromoStory>) {
const [w, h] = size;
const hasImage = story.src && story.mediaType === 'image';
return (
<div style={{ width: w, height: h, background: story.bgGradient ?? '#000', position: 'relative' }}>
{hasImage && <ImageStorySlide src={story.src} onLoad={onReady} onError={onError} />}
<div style={{ position: 'relative', zIndex: 1, textAlign: 'center', padding: 32, color: '#fff' }}>
<h2>{story.title}</h2>
{story.subtitle && <p>{story.subtitle}</p>}
{story.ctaText && (
<button style={{ marginTop: 16, padding: '8px 24px', borderRadius: 20, background: '#fff', color: '#000', border: 'none' }}>
{story.ctaText}
</button>
)}
</div>
</div>
);
}
<StoriesOverlay<PromoStory>
isOpen={isOpen}
onClose={handleClose}
groups={groups}
renderSlide={(props) => <CustomSlide {...props} />}
/>CSS クラス
CSS クラスはすべて通常のクラス(CSS Modules ではありません)なので、@reelkit/react-stories-player/styles.css のあとに読み込むスタイルシートで、詳細度の高いセレクターを使って指定できます。色、サイズ、z-index を変えるなら、下の テーマ設定 で説明している CSS カスタムプロパティを使ってください。
| クラス | コンポーネント | 説明 |
|---|---|---|
.rk-stories-overlay | Overlay | 固定配置のフルスクリーンの背景(背景色、z-index) |
.rk-stories-swipe-wrapper | Overlay | スワイプで閉じるためのラッパー(ナビゲーションボタンと canvas を含みます) |
.rk-stories-container | Overlay | 角の丸いストーリーの表示領域(配置、はみ出し) |
.rk-stories-ui-layer | Overlay | UI オーバーレイのコンテナー(ヘッダー、進捗、ナビゲーション)。chromePlacement="group" では各グループのスライドの中にあります |
.rk-stories-ui-layer--hidden | Overlay | UI を隠した状態(hideUIOnPause で切り替わります) |
.rk-stories-error | Overlay | エラー状態(中央のアイコンとテキスト) |
.rk-stories-error-text | Overlay | エラーメッセージのテキスト |
.rk-stories-nav-btn | Navigation | デスクトップの前後の矢印 |
.rk-stories-progress-bar | ProgressBar | canvas の進捗バーを配置するラッパー |
.rk-stories-slide-wrapper | Group | ストーリーの 1 グループ(外側のスライド) |
.rk-stories-story | Story | 1 つのストーリー(内側のスライドの ルート) |
.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-btn--desktop | StoryHeader | 一時停止ボタン。幅 768px を超えるときだけ表示されます |
.rk-stories-header-spinner | StoryHeader | 動画のバッファリングのスピナー |
.rk-stories-image | ImageStorySlide | 画像のストーリーの要素 |
.rk-stories-video | VideoStorySlide | 動画のストーリーのコンテナー |
.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 | ストーリーのリング(アニメーションするグラデーションの枠付きアバター) |
.rk-stories-ring--active | StoriesRing | まだ見るストーリーが残っているリング(アニメーションします) |
.rk-stories-ring-avatar | StoriesRing | リングの中のアバター画像 |
.rk-stories-ring-list | StoriesRingList | 横に並ぶリングのリストのコンテナー |
.rk-stories-ring-list-item | StoriesRingList | リングと名前の列 |
.rk-stories-ring-list-name | StoriesRingList | 各リングの下の作者の名前 |
.rk-stories-overlay--carousel | Overlay | デスクトップのカルーセルを表示中のオーバーレイ |
.rk-stories-overlay--sliding | Overlay | カルーセルのスライド中。プレーヤーのキャンバスは非表示 |
.rk-stories-carousel | Carousel | プレーヤーの背後にあるプレビューカードのレイヤー |
.rk-stories-carousel--instant | Carousel | スライドの最初のフレーム。トランジションなしで配置 |
.rk-stories-card | Carousel | 配置済みのプレビューカード 1 枚 |
.rk-stories-card--center | Carousel | スライド中に中央の位置にあるカード |
.rk-stories-card--hidden | Carousel | 片側に表示する 2 枚より外側のカード |
.rk-stories-card-button | Carousel | デフォルトのカードの中身(グループを開くボタン) |
.rk-stories-card-image | Carousel | プレビューの画像(ポスターまたは画像) |
.rk-stories-card-frame | Carousel | 画像もポスターもないストーリーを renderSlide で縮小して描いたプレビュー。操作は受け付けません |
.rk-stories-card-scrim | Carousel | 画像の上に重ねる暗いレイヤー |
.rk-stories-card-info | Carousel | リング、作成者名、時間を並べる列 |
.rk-stories-card-name | Carousel | 作成者名 |
.rk-stories-card-time | Carousel | 経過時間のテキスト |
テーマ設定
色、サイズ、z-index、トランジションはすべて CSS カスタムプロパティにあります。:root(またはオーバーレイの任意の祖先)で 1 つでも複数でも上書きすれば、コンポーネントのソースに触れずにテーマを変えられます。
| トークン | デフォルト | 制御するもの |
|---|---|---|
--rk-stories-overlay-bg | #000 | フルスクリーンの背景色 |
--rk-stories-overlay-z | 9999 | オーバーレイの z-index |
--rk-stories-swipe-gap | 16px | ナビゲーションボタンとストーリーの表示領域の間隔 |
--rk-stories-container-radius | 12px | ストーリーの表示領域の角丸(デスクトップ) |
--rk-stories-container-radius-mobile | 0 | 幅 768px までのストーリーの表示領域の角 |
--rk-stories-ui-z | 15 | UI レイヤー( ヘッダー、進捗バー)の z-index |
--rk-stories-ui-transition | 200ms | hideUIOnPause が切り替わるときのフェードの長さ |
--rk-stories-top-shade-height | 120px | ヘッダーの後ろの上部のグラデーションの高さ |
--rk-stories-top-shade-bg | linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%) | 上部のグラデーションの色 |
--rk-stories-error-z | 5 | エラー状態の z-index |
--rk-stories-error-bg | linear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%) | エラー状態の背景のグラデーション |
--rk-stories-error-fg | rgba(255, 255, 255, 0.5) | エラーアイコンとテキストの色 |
--rk-stories-error-gap | 12px | エラーアイコンとテキストの間隔 |
--rk-stories-error-text-size | 13px | エラーメッセージの文字サイズ |
--rk-stories-nav-z | 20 | デスクトップのナビゲーションボタンの z-index |
--rk-stories-nav-size | 44px | デスクトップの前後のボタンの大きさ |
--rk-stories-nav-bg | rgba(255, 255, 255, 0.1) | デスクトップのナビゲーションボタンの背景 |
--rk-stories-nav-bg-hover | rgba(255, 255, 255, 0.2) | デスクトップのナビゲーションボタンのホバー時の背景 |
--rk-stories-nav-fg | rgba(255, 255, 255, 0.7) | デスクトップのナビゲーションボタンのアイコンの色 |
--rk-stories-nav-fg-hover | #fff | デスクトップのナビゲーションボタンのホバー時のアイコンの色 |
--rk-stories-nav-transition | 150ms | ナビゲーションボタンの背景と色のトランジション |
--rk-stories-video-bg | #000 | <video> の後ろのレターボックスの背景 |
--rk-stories-video-poster-transition | 200ms | 動画の再生が始まるときのポスターのフェードの長さ |
--rk-stories-progress-bar-z | 10 | 進捗バーの z-index |
--rk-stories-progress-bar-padding | 8px 8px 0 | 進捗バーの周りの余白 |
--rk-stories-header-z | 5 | ヘッダーの z-index |
--rk-stories-header-top | 18px | ストーリーの上端からのヘッダーの縦の位置 |
--rk-stories-header-padding | 12px 16px | ヘッダーの行の内側の余白 |
--rk-stories-header-gap | 8px | アバター、名前、時間の間隔 |
--rk-stories-header-transition | 200ms | ヘッダーが隠れるときのフェード |
--rk-stories-header-avatar-size | 32px | アバターの幅と高さ |
--rk-stories-header-name-fg | #fff | 作者の名前の色 |
--rk-stories-header-name-size | 14px | 作者の名前の文字サイズ |
--rk-stories-header-name-weight | 600 | 作者の名前の文字の太さ |
--rk-stories-header-time-fg | rgba(255, 255, 255, 0.6) | 「何分前」のテキストの色 |
--rk-stories-header-time-size | 12px | 「何分前」のテキストの文字サイズ |
--rk-stories-header-actions-gap | 8px | ヘッダーの操作ボタンの間隔 |
--rk-stories-header-btn-fg | #fff | ヘッダーの操作アイコンの色(閉じる、ミュート、一時停止) |
--rk-stories-header-btn-padding | 4px | ヘッダーの操作ボタンの内側の余白 |
--rk-stories-header-spinner-size | 20px | 読み込み中のスピナーの大きさ |
--rk-stories-header-spinner-track | rgba(255, 255, 255, 0.3) | 読み込み中のスピナーのトラックの色 |
--rk-stories-header-spinner-fg | #fff | 読み込み中のスピナーの弧の色 |
--rk-stories-header-spinner-duration | 0.8s | 読み込み中のスピナーが 1 回転する長さ |
--rk-stories-heart-z | 20 | ダブルタップのハートの z-index |
--rk-stories-heart-duration | 800ms | 飛び出してフェードアウトするアニメーションの長さ |
--rk-stories-ring-spin-duration | 4s | まだ見るストーリーが残っているリングの回転の長さ |
--rk-stories-ring-active-scale | 0.95 | 押している間のリングの拡大率 |
--rk-stories-ring-active-gradient | conic-gradient(from 180deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888, #f09433) | 未読のストーリーが残っているリングの表示。CSS の画像か色を指定できます。明るいページでは :root ではなく .rk-stories-ring-list に設定してください。プレーヤーのカルーセルの色が保たれます |
--rk-stories-ring-viewed-color | rgba(255, 255, 255, 0.25) | すべて見終えたリングの表示で、暗いページ向けの値です。明るいページでは :root ではなく .rk-stories-ring-list に設定してください |
--rk-stories-ring-gradient | none | 内部用:リングが描画のたびに書き込み、リングに gradientColors や viewedColor が渡されない限り上の 2 つのトークンを参照します |
--rk-stories-ring-list-gap | 12px | リスト内のリングの間隔 |
--rk-stories-ring-list-padding | 12px | リングのリストの内側の余白 |
--rk-stories-ring-list-item-gap | 4px | リングとその名前の間隔 |
--rk-stories-ring-list-name-size | 12px | 各リングの下の作者の名前の文字サイズ |
--rk-stories-card-bg | #262626 | カルーセルのカードの背景。プレビュー画像がないときに見えます |
--rk-stories-card-radius | 8px | カルーセルのカードの角丸 |
--rk-stories-card-scrim | rgba(0, 0, 0, 0.45) | 横のカードに重ねる暗さ |
--rk-stories-card-fg | #fff | カードの文字色 |
--rk-stories-card-name-size | 14px | カードの作成者名の文字サイズ |
--rk-stories-card-time-fg | rgba(255, 255, 255, 0.7) | カードの経過時間の文字色 |
--rk-stories-card-time-size | 13px | カードの経過時間の文字サイズ |
--rk-stories-card-gap | 6px | カード上のリング、名前、時間の間隔 |
--rk-stories-card-transition | 300ms | カルーセルのスライドの長さ |
下のスニペットを、@reelkit/react-stories-player/styles.css のあとに読み込むスタイルシートに入れてください。
/* Brand the stories overlay */
:root {
--rk-stories-overlay-bg: #0f172a;
--rk-stories-container-radius: 24px;
--rk-stories-nav-bg: rgba(99, 102, 241, 0.25);
--rk-stories-nav-bg-hover: rgba(168, 85, 247, 0.55);
--rk-stories-top-shade-bg: linear-gradient(
to bottom,
rgba(99, 102, 241, 0.5) 0%,
transparent 100%
);
--rk-stories-header-name-fg: #fef3c7;
--rk-stories-ring-spin-duration: 2s;
}
/* Rings on a light page. Set on the ring list, not :root, so the rings of the
player's own carousel keep the colours made for its dark backdrop. */
.rk-stories-ring-list {
--rk-stories-ring-viewed-color: #d4d4d8;
}アクセシビリティ
オーバーレイのルートはモーダルダイアログ(role="dialog"、aria-modal="true")です。ariaLabel を指定すると、スクリーンリーダーの読み上げが変わります。デフォルトは「Stories player」です。
オーバーレイは開くとフォーカスを取り込み、閉じるとトリガーに戻します。Tab と Shift+Tab は中のフォーカス可能な要素を循環し、外へ出たフォーカス(外側のクリック、プログラムからのフォーカス)は引き戻されます。@reelkit/core の captureFocusForReturn と createFocusTrap(@reelkit/react が再エク スポート)で実装されています。
デスクトップのカルーセルでは、デフォルトのカードはそれぞれ「Open stories by」と作成者名のラベルを持つボタンです。カードは Tab の順序でプレーヤーの操作ボタンのあとに来て、スライドの間はその順序から外れます。
サーバーサイドレンダリング
オーバーレイは閉じている間は何も描画せず、開いたときに初めて document.body にポータルします。これはクライアントで起こります。StoriesUrlOverlay はマウント後のエフェクトでアドレスバーを読むので、サーバーでの描画で開くことはありません。StoriesRingList はサーバーではすべてのリングを未視聴として描画します。視聴済みのストアはマウント後にしか読まれないため、サーバーのマークアップとクライアントの最初の描画が一致します。全体像は SSR ガイド を参照してください。
キーボードショートカット
| キー | 動作 |
|---|---|
ArrowLeft | 前のストーリー。グループの最初のストーリーでは前のグループへ移り、離れたときのストーリー(なければ再開位置のストーリー)を開きます。最初のグループでは何もしません |
ArrowRight | 次のストーリー。グループの最後のストーリーを過ぎると次のグループへ移り、最後のグループを過ぎるとプレイヤーが閉じます |
Escape | プレイヤーを閉じる |