Reel Player
Повноекранний компонент відеоплеєра у стилі Instagram Reels чи TikTok на основі @reelkit/react-reel-player.
Features
Встановлення
npm install @reelkit/react-reel-player @reelkit/react lucide-reactНе забудьте імпортувати стилі:
import '@reelkit/react-reel-player/styles.css';Icons
Стандартні елементи керування використовують lucide-react для іконок. Якщо ви віддаєте перевагу іншій бібліотеці іконок, скористайтеся renderControls та renderNavigation , щоб передати власні.
Швидкий старт
The ReelPlayerOverlay рендерить повноекранний оверлей плеєра. Передайте масив ContentItem і керуйте видимістю через isOpen.
import { useState } from 'react';
import { ReelPlayerOverlay, type ContentItem } from '@reelkit/react-reel-player';
import '@reelkit/react-reel-player/styles.css';
const content: ContentItem[] = [
{
id: '1',
media: [{
id: 'v1',
type: 'video',
src: 'https://example.com/video.mp4',
poster: 'https://example.com/poster.jpg',
aspectRatio: 9 / 16,
}],
author: { name: 'John Doe', avatar: 'https://example.com/avatar.jpg' },
likes: 1234,
description: 'Amazing video!',
},
];
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open Player</button>
<ReelPlayerOverlay
isOpen={isOpen}
onClose={() => setIsOpen(false)}
content={content}
/>
</>
);
}import { useState } from 'react';
import { ReelPlayerOverlay, type ContentItem } from '@reelkit/react-reel-player';
import '@reelkit/react-reel-player/styles.css';
const content: ContentItem[] = [
{
id: '1',
media: [{
id: 'v1',
type: 'video',
src: '/cdn/samples/videos/video-01.mp4',
poster: '/cdn/samples/videos/video-poster-01.jpg',
aspectRatio: 16 / 9,
}],
author: { name: 'Alex Johnson', avatar: '/cdn/samples/avatars/avatar-01.jpg' },
likes: 1234,
description: 'Amazing sunset vibes',
},
{
id: '2',
media: [{
id: 'img1',
type: 'image',
src: '/cdn/samples/images/image-01.jpg',
aspectRatio: 2 / 3,
}],
author: { name: 'Sarah Miller', avatar: '/cdn/samples/avatars/avatar-02.jpg' },
likes: 5678,
description: 'Nature at its finest',
},
{
id: '3',
media: [{
id: 'v2',
type: 'video',
src: '/cdn/samples/videos/video-04.mp4',
poster: '/cdn/samples/videos/video-poster-04.jpg',
aspectRatio: 16 / 9,
}],
author: { name: 'James Wilson', avatar: '/cdn/samples/avatars/avatar-03.jpg' },
likes: 3456,
description: 'City life adventures',
},
{
id: '4',
media: [
{ id: 'img2', type: 'image', src: '/cdn/samples/images/image-02.jpg', aspectRatio: 2 / 3 },
{ id: 'img3', type: 'image', src: '/cdn/samples/images/image-03.jpg', aspectRatio: 3 / 4 },
],
author: { name: 'Emma Davis', avatar: '/cdn/samples/avatars/avatar-04.jpg' },
likes: 8901,
description: 'Travel moments',
},
{
id: '5',
media: [{
id: 'img4',
type: 'image',
src: '/cdn/samples/images/image-04.jpg',
aspectRatio: 2 / 3,
}],
author: { name: 'Michael Brown', avatar: '/cdn/samples/avatars/avatar-05.jpg' },
likes: 2345,
description: 'Golden hour magic',
},
{
id: '6',
media: [{
id: 'v3',
type: 'video',
src: '/cdn/samples/videos/video-05.mp4',
poster: '/cdn/samples/videos/video-poster-05.jpg',
aspectRatio: 16 / 9,
}],
author: { name: 'Alex Johnson', avatar: '/cdn/samples/avatars/avatar-01.jpg' },
likes: 7890,
description: 'Living the moment',
},
];
export default function App() {
const [isOpen, setIsOpen] = useState(false);
const [initialIndex, setInitialIndex] = useState(0);
return (
<div style={{ padding: 16, background: '#0f172a', minHeight: '100vh' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 }}>
{content.map((item, i) => (
<button
key={item.id}
onClick={() => { setInitialIndex(i); setIsOpen(true); }}
style={{
position: 'relative', aspectRatio: '9 / 16', borderRadius: 8,
overflow: 'hidden', border: 'none', padding: 0, cursor: 'pointer',
background: '#1e293b',
}}
>
<img
src={item.media[0].poster || item.media[0].src}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
/>
</button>
))}
</div>
<ReelPlayerOverlay
isOpen={isOpen}
onClose={() => setIsOpen(false)}
content={content}
initialIndex={initialIndex}
/>
</div>
);
}Повний сценарій, сітку мініатюр, що відкриває плеєр і повертає глядача туди, де він зупинився, показує рецепт «Від сітки до плеєра».
Customization
Узагальнений тип вмісту
Використовуйте власні типи даних, розширивши BaseContentItem:
import { ReelPlayerOverlay, type BaseContentItem } from '@reelkit/react-reel-player';
interface MyItem extends BaseContentItem {
title: string;
username: string;
}
const items: MyItem[] = [
{
id: '1',
media: [{ id: 'v1', type: 'video', src: '/video.mp4', aspectRatio: 9/16 }],
title: 'My Video',
username: '@user',
},
];
<ReelPlayerOverlay<MyItem>
isOpen={isOpen}
onClose={handleClose}
content={items}
renderSlideOverlay={(item) => (
<div style={{ position: 'absolute', bottom: 16, left: 16, color: '#fff' }}>
<strong>{item.username}</strong>
<p>{item.title}</p>
</div>
)}
/>Власний оверлей слайда
Замініть вбудований оверлей слайда власним вмістом для кожного слайда:
<ReelPlayerOverlay
isOpen={isOpen}
onClose={handleClose}
content={content}
renderSlideOverlay={(item, index, isActive) => (
<div style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: 16,
background: 'linear-gradient(transparent, rgba(0,0,0,0.8))',
color: '#fff',
}}>
<h3>{item.author.name}</h3>
<p>{item.description}</p>
<span>Slide {index + 1} {isActive ? '(active)' : ''}</span>
</div>
)}
/>Слайди без медіа
Використовуйте renderSlide щоб вставити власний вміст (наприклад, картки із закликом до дії). Поверніть null щоб лишити стандартний:
<ReelPlayerOverlay
isOpen={isOpen}
onClose={handleClose}
content={content}
renderSlide={({ index, size }) => {
// CTA card on last slide
if (index === content.length - 1) {
return (
<div style={{
width: size[0],
height: size[1],
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #667eea, #764ba2)',
color: '#fff',
}}>
<div style={{ textAlign: 'center' }}>
<h2>Follow for more!</h2>
<button>Subscribe</button>
</div>
</div>
);
}
// Fall back to default MediaSlide + overlay
return null;
}}
/>Власні елементи керування
Складайте готові підкомпоненти разом із власними доповненнями:
import {
ReelPlayerOverlay,
CloseButton,
SoundButton,
} from '@reelkit/react-reel-player';
<ReelPlayerOverlay
isOpen={isOpen}
onClose={handleClose}
content={content}
renderControls={({ onClose, content, activeIndex }) => (
<>
<CloseButton onClick={onClose} />
<SoundButton />
<button
onClick={() => share(content[activeIndex])}
style={{
position: 'absolute',
bottom: 60,
right: 16,
zIndex: 10,
}}
>
Share
</button>
</>
)}
/>Власний таймлайн
Замініть вбудовану смугу відтворення власним інтерфейсом перемотування через renderTimeline. Колбек спрацьовує лише тоді, коли за правилами показу оверлей вивів би стандартну смугу (та сама логіка timeline плюс timelineMinDurationSeconds ), тож переписувати її не доведеться. Використайте клас .rk-reel-timeline на своєму корені, щоб успадкувати притиснення до низу, відступи безпечної зони та проміжок для оверлея слайда на дотикових пристроях.
import { useRef, useEffect } from 'react';
import { ReelPlayerOverlay } from '@reelkit/react-reel-player';
import { Observe } from '@reelkit/react';
function CustomTimelineBar({ timelineState }) {
const trackRef = useRef(null);
useEffect(() => {
if (!trackRef.current) return;
// Pointer + keyboard scrub wiring, same as the built-in bar.
return timelineState.bindInteractions(trackRef.current);
}, [timelineState]);
return (
<div className="rk-reel-timeline" style={{ padding: '0 16px' }}>
<Observe signals={[timelineState.progress, timelineState.currentTime]}>
{() => (
<div
ref={trackRef}
role="slider"
aria-valuenow={timelineState.currentTime.value}
style={{ height: 6, background: 'rgba(255,255,255,0.2)' }}
>
<div style={{
width: `${timelineState.progress.value * 100}%`,
height: '100%',
background: 'linear-gradient(90deg, #6366f1, #ec4899)',
}} />
</div>
)}
</Observe>
</div>
);
}
<ReelPlayerOverlay
isOpen={isOpen}
onClose={handleClose}
content={content}
timeline="always"
renderTimeline={({ timelineState }) => (
<CustomTimelineBar timelineState={timelineState} />
)}
/>Власна навігація
<ReelPlayerOverlay
isOpen={isOpen}
onClose={handleClose}
content={content}
renderNavigation={({ onPrev, onNext, activeIndex, count }) => (
<div style={{ position: 'absolute', right: 16, top: '50%', transform: 'translateY(-50%)' }}>
<button onClick={onPrev} disabled={activeIndex === 0}>Up</button>
<span>{activeIndex + 1}/{count}</span>
<button onClick={onNext} disabled={activeIndex === count - 1}>Down</button>
</div>
)}
/>Власна вкладена навігація
Замініть стрілки ліворуч і праворуч усередині мультимедійних слайдів (горизонтальна карусель) власною навігацією:
<ReelPlayerOverlay
isOpen={isOpen}
onClose={handleClose}
content={content}
renderNestedNavigation={({ onPrev, onNext, activeIndex, count }) => (
<div style={{
position: 'absolute',
bottom: 48,
left: 0,
right: 0,
display: 'flex',
justifyContent: 'center',
gap: 8,
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>
)}
/>Власні вкладені слайди
Налаштуйте окремі слайди всередині мультимедійних каруселей через renderNestedSlide. Use props.defaultContent щоб загорнути стандартний ImageSlide чи VideoSlide або замінити його повністю:
// Wrap default slides with rounded corners
<ReelPlayerOverlay
isOpen={isOpen}
onClose={handleClose}
content={content}
renderNestedSlide={({ defaultContent }) => (
<div style={{ borderRadius: 16, overflow: 'hidden' }}>
{defaultContent}
</div>
)}
/>
// Fully custom nested slide for images
<ReelPlayerOverlay
isOpen={isOpen}
onClose={handleClose}
content={content}
renderNestedSlide={({ item, size, isActive, slideKey, onVideoRef, defaultContent }) => {
if (item.type === 'video') return defaultContent; // keep default video
return (
<ImageSlide
src={item.src}
size={size}
imgStyle={{ objectFit: 'contain' }}
style={{ backgroundColor: '#111' }}
/>
);
}}
/>Стан в URL
Подивитися демо наживо →ReelPlayerUrlOverlay — окремий компонент, стан відкриття якого живе в адресному рядку. Побудуйте контролер через useOverlayUrlState from @reelkit/react і передайте його як controller: плеєр відкривається сам, коли параметр називає слайд, і закривається, коли параметр зникає. Посиланнями можна ділитися, а кнопка «назад» закриває плеєр, а не виводить зі сторінки.
Вбудовані клавіші
Слайди можна адресувати вбудованим ключем — розгорніть urlIndexKey (за позицією) або urlStableIdKey (за стабільним id) into the controller — both re-exported from @reelkit/react. See the посібник зі стану в URL та API ядра.
import { useOverlayUrlState, urlIndexKey, urlStableIdKey } from '@reelkit/react';
import { ReelPlayerUrlOverlay } from '@reelkit/react-reel-player';
import { Link } from 'react-router-dom';
const reel = useOverlayUrlState({
param: 'reel',
...urlIndexKey(() => content.length),
});
// Opening is a link — the overlay reads the URL and opens itself.
{content.map((item, i) => (
<Link key={item.id} to={`?reel=${i}`}>
<img src={getThumbnail(item)} />
</Link>
))}
<ReelPlayerUrlOverlay controller={reel} content={content} />Full useOverlayUrlState опції — param, adapter, codec, locator — у довіднику API для React. Покроковий розбір — у посібнику для React.
- Відкриття додає один запис в історію. Свайп стрічки замінює його, тож N свайпів не додають записів, і один крок назад завжди виходить із плеєра. «Назад» закриває, а не гортає слайди.
- «Назад» закриває лише тоді, коли плеєр відкрили всередині застосунку — посилання додало запис. За надісланим посиланням у новій вкладці історії позаду немає, тож кнопка «назад» виведе із сайту; кнопка ✕ або Escape прибирає параметр на місці й лишає вас на сторінці.
- Пряме посилання
?reel=3відкриває плеєр одразу на цьому слайді. - Параметр, який не називає жодного слайда — застаріла закладка, змінене вручну значення, — прибирається з URL, а не лишає в адресному рядку слайд, який не відкриється.
- Типово параметр адресує лише vertical допис (
?reel=3). Opt into a two-axis key to also carry the inner media index of a multi-media carousel — see below.
Один ключ чи два — оберіть глибину URL
Той самий ReelPlayerUrlOverlay працює з обома формами; він розрізняє їх під час виконання за position контролера, тож жодного пропса режиму немає. Ключ обирайте, коли будуєте контролер:
| Key | Wire | Carries |
|---|---|---|
urlIndexKey(…) | ?reel=3 | Лише вертикальний допис. |
urlIndexTwoAxisKey(…) | ?reel=3.2 | Допис та та індекс внутрішнього медіа каруселі. |
Дві форми запису навмисно різні — двовісний ключ строго з крапкою (3.0, never a bare 3), so a bare one-axis link does not cross-decode. Switching an app between keys therefore invalidates any previously shared links. Pick one shape and keep it.
import { useOverlayUrlState, urlIndexTwoAxisKey } from '@reelkit/react';
const reel = useOverlayUrlState({
param: 'reel',
...urlIndexTwoAxisKey({
outerCount: () => content.length,
innerCounts: () => content.map((post) => post.media.length),
}),
});
// A link now names both axes: post 3, inner media 2.
<Link to="?reel=3.2">…</Link>
<ReelPlayerUrlOverlay controller={reel} content={content} />Застосунок із роутером — передайте адаптер. Writing history.pushState повз роутер лишає його місцеположення застарілим, і наступна навігація втрачає параметр:
import { useReactRouterUrlAdapter } from '@reelkit/react/react-router-url-adapter';
const adapter = useReactRouterUrlAdapter();
const reel = useOverlayUrlState({
param: 'reel',
adapter,
...urlIndexKey(() => content.length),
});
<ReelPlayerUrlOverlay controller={reel} content={content} />Стабільні посилання. Індекс адресує за позицією, тож збережений у закладках ?reel=3 відкриє інший допис, щойно стрічку перевпорядкують — а для стрічки це радше правило, ніж виняток. urlStableIdKey адресує за стабільним id, scanning the live feed — one call covers the common case.
const reel = useOverlayUrlState({
param: 'reel',
...urlStableIdKey({ items: () => content }),
});Pass hashCodec: base64UrlCodec щоб закодувати id в URL у base64url — оборотне маскування, а не криптографічний хеш.
Адресуєте за іншим полем ( slug), or page an infinite feed with locateAsync, and build the codec/locator самі. Дві окремі задачі: codec записує ідентичність в URL, locator знаходить, де ця ідентичність лежить.
const reel = useOverlayUrlState({
param: 'reel',
codec: { decode: (raw) => raw, encode: (id) => id },
locator: {
locate: (id) => content.findIndex((x) => x.id === id),
identify: (index) => content[index].id,
},
});Нескінченні стрічки. locate синхронний, тож відповідає лише за вже завантажені дописи — надіслане посилання на допис 400 у стрічці, де завантажено 20, нічого не знайде. locateAsync — запасний варіант, що викликається лише коли locate misses.
Shortcut
Keying by the item’s id? Не пишіть кодек і локатор вручну — передайте locateAsync просто в urlStableIdKey({ items, locateAsync }) (він вантажить дані, якщо не знайшов, і повертає індекс). Розгорнутий варіант нижче — для адресації за іншим полем або для повного контролю.
const reel = useOverlayUrlState({
param: 'reel',
codec: { decode: (raw) => raw, encode: (id) => id },
locator: {
locate: (id) => content.findIndex((x) => x.id === id),
identify: (index) => content[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 post
setContent(loaded); // commit — the overlay renders from this state
return loaded.findIndex((x) => x.id === id); // wherever it landed
},
},
});- While
locateAsyncу процесі, плеєр лишається закритим, а параметр — недоторканим, тож пряме посилання переживає запит.nullабо відмова прибирає параметр. - Відповідь, що приходить після зміни URL, після закриття або після демонтажу, відкидається — повільний запит не відкриє слайд, якого ніхто не просив.
- Поки триває очікування, нічого не рендериться: цей стан завантаження вже належить сторінці, тож малюйте власний скелетон.
- Тайм-ауту немає — плеєр не може знати, яка стрічка завдовжки. Завершуйте значенням
nullколи сторінки скінчилися, інакше оверлей лишиться закритим назавжди.
Довідник API
Пропси ReelPlayerOverlayProps
ReelPlayerOverlayProps<T>
ReelPlayerOverlayProps — це ReelPlayerOverlayBaseProps (візуальні та поведінкові пропси, спільні для обох оверлеїв), поєднаний із ReelPlayerControlledProps (isOpen і onClose). Базовим типом типізуйте обгортку, що рендерить будь-який з оверлеїв.
| Prop | Тип | Типове значення | Опис |
|---|---|---|---|
apiRef | MutableRefObject<ReelApi> | - | Ref для доступу до API Reel |
ariaLabel | string | 'Video player' | Доступна назва області діалогу; екранні читачі оголошують її, коли оверлей відкривається |
aspectRatio | number | 9/16 (0.5625) | Співвідношення ширини до висоти контейнера плеєра на десктопі. На ширині 768px і вужче плеєр завжди займає всю область перегляду. Нульове, від’ємне чи нескінченне значення замінюється на 9/16. |
content | T[] | required | Масив елементів вмісту (узагальнений тип, типово ContentItem) |
initialIndex | number | 0 | Початковий індекс слайда |
initialInnerIndex | number | 0 | Індекс внутрішнього медіа, з якого відкриватися, лише для початково видимого допису — дає змогу двовісному URL вести просто до потрібного зображення мультимедійного допису. Ігнорується, щойно плеєр відкрито й користувач починає гортати. |
isOpen | boolean | required | Керує видимістю оверлея. Для стану відкриття, керованого URL, беріть окремий ReelPlayerUrlOverlay — дивіться розділ про стан в URL нижче. |
timeline | 'auto' | 'always' | 'never' | 'auto' | Правило показу вбудованої смуги таймлайну. 'auto' показує її лише для відео, довших за timelineMinDurationSeconds; 'always' — щойно на активному слайді є відео; 'never' вимикає вбудовану смугу (для повної заміни беріть renderTimeline). |
timelineMinDurationSeconds | number | 30 | Мінімальна тривалість відео (у секундах), за якої timeline='auto' показує вбудовану смугу. Короткі зациклені кліпи нижче цього порогу не показують її. |
renderControls | (props: ControlsRenderProps) => ReactNode | - | Власні елементи керування, замінюють стандартні кнопки закриття та звуку |
renderError | (props: { item: T; activeIndex: number }) => ReactNode | - | Власний індикатор помилки, замінює стандартний значок |
renderLoading | (props: { item: T; activeIndex: number }) => ReactNode | - | Власний індикатор завантаження, замінює стандартну хвилю |
renderNavigation | (props: NavigationRenderProps) => ReactNode | - | Власна навігація, замінює стандартні вертикальні стрілки |
renderNestedNavigation | (props: NavigationRenderProps) => ReactNode | - | Власна навігація для вкладеного горизонтального слайдера (мультимедійні дописи), замінює стандартні стрілки ліворуч і праворуч |
renderNestedSlide | (props: NestedSlideRenderProps) => ReactNode | - | Власний рендерер слайдів вкладеного горизонтального слайдера. Через props.defaultContent можна загорнути або вбудувати стандартний ImageSlide чи VideoSlide. На відміну від renderSlide, null тут не озна чає повернення до стандартного. |
renderSlide | (props: SlideRenderProps) => ReactNode | null | - | Власний рендеринг слайда. Поверніть null, щоб лишити стандартний. Через props.defaultContent можна загорнути або вбудувати стандартний слайд. |
renderSlideOverlay | (item, index, isActive) => ReactNode | - | Власний оверлей для кожного слайда, замінює стандартний SlideOverlay. Поверніть null, щоб сховати. |
renderTimeline | (props: TimelineRenderProps) => ReactNode | - | Власна смуга таймлайну відтворення. Викликається лише тоді, коли за правилами показу вивелася б стандартна смуга (та сама логіка auto/always/never плюс timelineMinDurationSeconds). Через props.defaultContent можна загорнути вбудований <TimelineBar />; поверніть null, щоб сховати. |
Пропси ReelPlayerUrlOverlay
ReelPlayerUrlOverlayProps<T>
Приймає всі візуальні та поведінкові пропси вище, крім isOpen, замість якого приходить controller: тип — ReelPlayerOverlayBaseProps, поєднаний із ReelPlayerUrlControlledProps. initialIndex ігнорується — слайд обирає position контролера, тож передане поруч значення перезаписується на кожному відкритті.
| Prop | Тип | Типове значення | Опис |
|---|---|---|---|
controller | UrlStateController | required | Контролер із useOverlayUrlState. Його position вирішує, чи оверлей відкритий і який слайд показує; оверлей записує через нього назад на зміну слайда та на закриття. |
Колбеки
| Prop | Тип | Опис |
|---|---|---|
onClose | () => void | Викликається, коли плеєр закривається. Обов’язковий у ReelPlayerOverlay (стан відкриття ваш, тож закриття обробляєте ви); необов’язковий у ReelPlayerUrlOverlay, де закриттям керує URL — передавайте лише щоб зреагувати після закриття. |
onSlideChange | (index: number) => void | Викликається після зміни слайда |
onInnerSlideChange | (outerIndex: number, innerIndex: number) => void | Викликається, коли змінюється індекс внутрішнього медіа активного допису — під час навігації всередині мультимедійного допису й під час активації зовнішнього, повідомляючи поточний внутрішній індекс активованого допису (0 для допису з одним медіа). |
Пропси Reel (передані далі)
Ці пропси передаються далі в компонент Reel. Їхній тип — ReelProxyProps, підмножина пропсів Reel, яку оверлей передає далі; типізуйте ним обгортку, що їх прокидає.
| Prop | Тип | Типове значення | Опис |
|---|---|---|---|
enableNavKeys | boolean | true | Вмикає навігацію з клавіатури |
enableWheel | boolean | true | Вмикає навігацію колесом миші |
loop | boolean | false | Вмикає нескінченний цикл |
swipeDistanceFactor | number | 0.12 | Поріг свайпу (0–1) |
transitionDuration | number | 300 | Тривалість анімації переходу (мс) |
wheelDebounceMs | number | 200 | Тривалість дебаунсу колеса (мс) |
Types
BaseContentItem
Тип-обмеження для узагальнення. Розширюйте його, щоб використовувати власні типи даних із ReelPlayerOverlay.
interface BaseContentItem {
id: string;
media: MediaItem[];
}ContentItem
interface ContentItem extends BaseContentItem {
author: {
name: string;
avatar: string;
};
likes: number;
description: string;
}MediaItem
interface MediaItem {
id: string;
type: 'image' | 'video';
src: string;
poster?: string;
aspectRatio: number;
}MediaType
type MediaType = 'image' | 'video';ControlsRenderProps<T>
interface ControlsRenderProps<T extends BaseContentItem> {
onClose: () => void;
soundState: SoundState;
activeIndex: number;
content: T[];
}NavigationRenderProps
interface NavigationRenderProps {
onPrev: () => void;
onNext: () => void;
activeIndex: number;
count: number;
}SlideRenderProps<T>
interface SlideRenderProps<T extends BaseContentItem> {
item: T;
index: number;
size: [number, number];
isActive: boolean;
slideKey: string;
onVideoRef?: (ref: HTMLVideoElement | null) => void;
innerSliderRef: MutableRefObject<ReelApi | null>;
onActiveMediaTypeChange?: (type: 'image' | 'video') => void;
renderNestedNavigation?: (props: NavigationRenderProps) => ReactNode;
enableWheel?: boolean;
defaultContent: ReactNode;
}NestedSlideRenderProps
interface NestedSlideRenderProps {
item: MediaItem;
index: number;
size: [number, number];
isActive: boolean;
isInnerActive: boolean;
slideKey: string;
onVideoRef?: (ref: HTMLVideoElement | null) => void;
defaultContent: ReactNode;
}SlideOverlayProps
interface SlideOverlayProps {
author?: { name: string; avatar: string };
description?: string;
likes?: number;
}ImageSlideProps
interface ImageSlideProps {
src: string;
size: [number, number];
className?: string;
style?: CSSProperties;
imgClassName?: string;
imgStyle?: CSSProperties;
}VideoSlideProps
interface VideoSlideProps {
src: string;
poster?: string;
aspectRatio: number;
size: [number, number];
isActive: boolean;
isInnerActive?: boolean; // default: true
slideKey: string;
onVideoRef?: (ref: HTMLVideoElement | null) => void;
className?: string;
style?: CSSProperties;
}CloseButtonProps
interface CloseButtonProps {
onClick: () => void;
className?: string;
style?: React.CSSProperties;
}SoundButtonProps
interface SoundButtonProps {
disabled?: boolean;
className?: string;
style?: React.CSSProperties;
}TimelineBarProps
interface TimelineBarProps {
className?: string;
style?: React.CSSProperties;
}TimelineRenderProps<T>
interface TimelineRenderProps<T extends BaseContentItem> {
item: T;
activeIndex: number;
timelineState: TimelineController;
defaultContent: ReactNode;
}Sub-Components
Повторно використовувані блоки, експортовані для складання власних render props:
CloseButton
Окрема кнопка закриття зі стандартними стилями плеєра. Використовуйте всередині renderControls.
import { CloseButton } from '@reelkit/react-reel-player';
<CloseButton onClick={onClose} />
<CloseButton onClick={onClose} className="my-close-btn" style={{ top: 24, right: 24 }} />SoundButton
Окремий перемикач звуку. Має бути всередині SoundProvider (ReelPlayerOverlay додає його автоматично).
import { SoundButton } from '@reelkit/react-reel-player';
<SoundButton />
<SoundButton disabled className="my-sound-btn" />TimelineBar
Стандартна смуга перемотування. Читає з найближчого TimelineProvider (автоматично монтується всередині ReelPlayerOverlay) and renders the track, buffered ranges, progress fill, and scrub pill. Theme via the --rk-reel-timeline-* власними властивостями або замініть через renderTimeline.
import { TimelineBar } from '@reelkit/react-reel-player';
// Inside renderTimeline — wrap or augment the default bar:
<ReelPlayerOverlay
renderTimeline={({ defaultContent }) => (
<>
<MyTimecode />
{defaultContent}
</>
)}
/>
// Or render standalone inside a custom TimelineProvider tree:
<TimelineBar className="my-timeline" />SlideOverlay
Стандартний градієнтний оверлей з автором, описом і лайками. Показується автоматично, коли у вмісті є потрібні поля. Через renderSlideOverlay його можна замінити або сховати.
import { SlideOverlay } from '@reelkit/react-reel-player';
<SlideOverlay
author={{ name: 'John', avatar: '/avatar.jpg' }}
description="Amazing content"
likes={12500}
/>ImageSlide
Слайд-зображення з лінивим завантаженням і object-fit: cover за замовчуванням. Використовуйте всередині renderSlide щоб складати власні слайди-зображення зі своїми стилями.
import { ImageSlide } from '@reelkit/react-reel-player';
// Default usage
<ImageSlide src="/photo.jpg" size={[400, 700]} />
// Custom styles
<ImageSlide
src="/photo.jpg"
size={[400, 700]}
className="my-image-slide"
style={{ backgroundColor: '#1a1a1a', borderRadius: 12 }}
imgStyle={{ objectFit: 'contain' }}
/>VideoSlide
Слайд-відео зі спільним елементом <video> заради безперервності звуку на iOS, з постерами, пам’яттю позиції та індикатором завантаження. Має бути всередині SoundProvider (ReelPlayerOverlay додає його автоматично).
import { VideoSlide } from '@reelkit/react-reel-player';
<VideoSlide
src="/video.mp4"
poster="/thumb.jpg"
aspectRatio={9 / 16}
size={[400, 700]}
isActive={true}
slideKey="slide-1"
style={{ borderRadius: 12 }}
/>Складання власних слайдів
Використовуйте renderSlide з ImageSlide / VideoSlide щоб змінити рендери нг медіа, зберігши всю вбудовану поведінку (автовідтворення, знімок постера, синхронізацію звуку).
import {
ReelPlayerOverlay,
ImageSlide,
VideoSlide,
} from '@reelkit/react-reel-player';
<ReelPlayerOverlay
isOpen={isOpen}
onClose={handleClose}
content={content}
renderSlide={({ item, size, isActive, slideKey, onVideoRef }) => {
const media = item.media[0];
if (media.type === 'image') {
return (
<ImageSlide
src={media.src}
size={size}
imgStyle={{ objectFit: 'contain' }}
style={{ backgroundColor: '#111' }}
/>
);
}
if (media.type === 'video') {
return (
<VideoSlide
src={media.src}
poster={media.poster}
aspectRatio={media.aspectRatio}
size={size}
isActive={isActive}
slideKey={slideKey}
onVideoRef={onVideoRef}
style={{ borderRadius: 16 }}
/>
);
}
return null; // fallback to default
}}
/>Завантаження вмісту та обробка помилок
Плеєр стежить за станом завантаження й помилок кожного слайда. Поки вміст вантажиться, показується хвильовий індикатор; для зіпсованого медіа — значок помилки. URL з помилками кешуються, тож повторний перехід одразу показує помилку без нової спроби.
Колбеки життєвого циклу
Якщо ви використовуєте renderSlide, call these callbacks to control the loading indicator:
| Колбек | Коли викликати |
|---|---|
onReady | Зображення завантажилося або відео почало грати. Скидає стани завантаження й помилки. |
onWaiting | Відео буферизується посеред відтворення. Показує індикатор завантаження. |
onError | Вміст не завантажився. Показує оверлей помилки й кешує URL як зіпсований. |
// Inside renderSlide — wire callbacks to your custom media
renderSlide={({ item, size, isActive, onReady, onWaiting, onError }) => (
<div style={{ width: size[0], height: size[1] }}>
{item.media[0].type === 'image' ? (
<img
src={item.media[0].src}
onLoad={onReady}
onError={onError}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
<video
src={item.media[0].src}
autoPlay={isActive}
onCanPlay={onReady}
onWaiting={onWaiting}
onError={onError}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
)}
</div>
)}Власний інтерфейс завантаження та помилок
Замініть стандартний хвильовий індикатор і значок помилки власни ми компонентами:
<ReelPlayerOverlay
isOpen={isOpen}
onClose={() => setIsOpen(false)}
content={content}
renderLoading={({ item, activeIndex }) => (
<div style={{
position: 'absolute', inset: 0, zIndex: 10,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 14,
}}>
Loading slide {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 media</span>
</div>
)}
/>Таймлайн
Оверлей показує вбудовану смугу таймлайну відтворення над активним відео. Пропс timeline типу TimelineMode визначає, коли її показувати:
'auto'(типово): показується, коли активне медіа — відео, тривалість якого перевищуєtimelineMinDurationSeconds(типово 30). Працює і для слайдів з одним відео, і для мультимедійних каруселей; смуга слідує за активним вкладеним елементом і ховається на зображеннях.'always': показується, щойно на активному слайді є відео.'never': не показується ніколи. Власну смугу будуйте черезrenderTimeline.
<ReelPlayerOverlay
isOpen={isOpen}
onClose={close}
content={items}
timeline="auto"
timelineMinDurationSeconds={30}
/>Тему задавайте через власні властивості CSS --rk-reel-timeline-* (висота, кольори, розмір повзунка). Для повністю власної смуги перемотування, таймкоду чи індикатора прогресу беріть renderTimeline; the callback receives a timelineState на основі TimelineController.
Компонент, відрендерений усередині оверлея, може сам прочитати той самий контролер: useTimelineState() повертає його з найближчого TimelineProvider (його монтує оверлей) і кидає помилку поза ним; useTimelineStateOptional() натомість повертає там null — для компонентів, які можуть рендеритися й поза плеєром.
Контекст звуку
Для власних реалізацій доступ до стану звуку відкритий:
import { SoundProvider, useSoundState } from '@reelkit/react';
// ReelPlayerOverlay wraps itself in a SoundProvider.
// Access sound state inside custom controls:
function CustomControls() {
const soundState = useSoundState();
return (
<button onClick={soundState.toggle}>
{soundState.muted.value ? 'Unmute' : 'Mute'}
</button>
);
}Класи CSS
Усі класи CSS звичайні (не CSS-модулі), тож їх можна перекрити селекторами вищої специфічності в таблиці стилів, підключеній після @reelkit/react-reel-player/styles.css. Для змін кольору, розміру та z-index краще беріть власні властивості CSS, описані в розділі Theming нижче — вони саме для цього й зроблені.
| Class | Component | Опис |
|---|---|---|
.rk-reel-overlay | Overlay | Фіксоване повноекранне тло (фон, z-index) |
.rk-reel-container | Overlay | Контейнер плеєра (позиція, переповнення) |
.rk-reel-loader | Overlay | Оверлей хвильової анімації завантаження |
.rk-reel-media-error | Overlay | Оверлей стану помилки (значок і текст по центру) |
.rk-reel-media-error-text | Overlay | Текст повідомлення про помилку |
.rk-reel-button | Controls | Спільна кругла кнопка з іконкою (закриття, звук, стрілки) |
.rk-reel-close-btn | Controls | Кнопка закриття |
.rk-reel-sound-btn | Controls | Кнопка перемикання звуку |
.rk-reel-nav-arrows | Навігація | Контейнер стрілок лише для десктопа (прихований на ширині 768px і вужче) |
.rk-reel-nav-button | Навігація | Окрема стрілка вперед або назад |
.rk-reel-slide-wrapper | Slide | Обгортка навколо медіа та оверлея |
.rk-reel-slide-overlay | SlideOverlay | Контейнер градієнтного оверлея |
.rk-reel-slide-overlay-author | SlideOverlay | Рядок автора (аватар та ім’я) |
.rk-reel-slide-overlay-avatar | SlideOverlay | Зображення аватара автора |
.rk-reel-slide-overlay-name | SlideOverlay | Текст імені автора |
.rk-reel-slide-overlay-description | SlideOverlay | Текст опису |
.rk-reel-slide-overlay-likes | SlideOverlay | Рядок лайків (сердечко й лічильник) |
.rk-reel-video-container | VideoSlide | Обгортка відео (фон, переповнення) |
.rk-reel-video-element | VideoSlide | Елемент <video> |
.rk-reel-video-poster | VideoSlide | Постер (зникає з початком відтворення) |
.rk-reel-video-poster.rk-visible | VideoSlide | Модифікатор стану постера, поки відео на паузі або вантажиться |
.rk-reel-nested-indicator | NestedSlider | Пагінація точками під мультимедійними слайдами (позиція різна на десктопі й на дотикових пристроях) |
.rk-reel-nested-nav | NestedSlider | Стрілки горизон тальної каруселі (приховані на ширині 768px і вужче) |
.rk-reel-nested-nav-next | NestedSlider | Позиція вкладеної стрілки вперед |
.rk-reel-nested-nav-prev | NestedSlider | Позиція вкладеної стрілки назад |
.rk-reel-timeline | TimelineBar | Обгортка смуги перемотування. Використовуйте на власних коренях `renderTimeline`, щоб успадкувати притиснення до низу, відступи безпечної зони та проміжок для оверлея слайда на дотикових пристроях. |
.rk-reel-timeline-track | TimelineBar | Доріжка (невідтворена частина) |
.rk-reel-timeline-buffered | TimelineBar | Шар буферизованих сегментів |
.rk-reel-timeline-fill | TimelineBar | Заповнення відтвореного прогресу |
.rk-reel-timeline-cursor | TimelineBar | Повзунок перемотування (плаває над доріжкою) |
Theming
Кожен колір, розмір, z-index і перехід живе у власній властивості CSS. Перевизначайте одну чи кілька на :root (або на будь-якому предку оверлея), щоб змінити тему, не чіпаючи код компонентів.
| Token | Типове значення | Controls |
|---|---|---|
--rk-reel-overlay-bg | #000 | Колір повноекранного фону |
--rk-reel-overlay-z | 1000 | z-index оверлею |
--rk-reel-button-bg | rgba(0, 0, 0, 0.5) | Типовий фон круглої кнопки |
--rk-reel-button-bg-hover | rgba(255, 255, 255, 0.1) | Фон стрілки навігації (і базовий стан наведення) |
--rk-reel-button-bg-hover-strong | rgba(255, 255, 255, 0.2) | Фон стрілки навігації під час наведення |
--rk-reel-button-fg | #fff | Колір іконки кнопки |
--rk-reel-button-size | 44px | Ширина й висота кнопки |
--rk-reel-button-radius | 50% | Радіус заокруглення кнопки |
--rk-reel-ui-z | 10 | z-index кнопок закриття, звуку й навігації |
--rk-reel-edge-padding | 16px | Відступ від краю для кнопок закриття, звуку та стрілок навігації |
--rk-reel-nav-gap | 8px | Відстань між розташованими одна над одною стрілками навігації |
--rk-reel-transition | 0.2s | Тривалість переходу під час наведення |
--rk-reel-loader-color | rgba(255, 255, 255, 0.12) | Колір градієнта хвильового завантажувача |
--rk-reel-loader-duration | 1.8s | Тривалість анімації хвильового завантажувача |
--rk-reel-error-fg | rgba(255, 255, 255, 0.4) | Колір іконки й тексту помилки |
--rk-reel-error-text-size | 13px | Розмір шри фту повідомлення про помилку |
--rk-reel-slide-overlay-bg | linear-gradient(transparent, rgba(0, 0, 0, 0.7)) | Градієнт затемнення під підписом |
--rk-reel-slide-overlay-padding | 48px 16px 16px | Внутрішній відступ підпису |
--rk-reel-slide-overlay-name-color | #fff | Колір імені автора |
--rk-reel-slide-overlay-description-color | rgba(255, 255, 255, 0.9) | Колір тексту опису |
--rk-reel-slide-overlay-likes-color | rgba(255, 255, 255, 0.8) | Колір тексту рядка вподобань |
--rk-reel-video-bg | #000 | Фон смуг навколо <video> |
--rk-reel-nested-button-bg | rgba(0, 0, 0, 0.5) | Фон вкладеної стрілки |
--rk-reel-nested-button-bg-hover | rgba(255, 255, 255, 0.2) | Фон вкладеної стрілки під час наведення |
--rk-reel-nested-button-size | 36px | Розмір вкладеної стрілки |
--rk-reel-nested-edge-padding | 12px | Відступ вкладеної стрілки від краю |
--rk-reel-timeline-track | rgba(255, 255, 255, 0.22) | Фон доріжки (невідтворена частина) |
--rk-reel-timeline-buffered | rgba(255, 255, 255, 0.4) | Колір буферизованих відрізків |
--rk-reel-timeline-fill | #fff | Колір заповнення відтвореної частини |
--rk-reel-timeline-cursor | #fff | Колір капсули повзунка перемотування |
--rk-reel-timeline-height | 3px | Висота доріжки у спокої |
--rk-reel-timeline-height-active | 6px | Висота доріжки під час наведення, фокусу й перемотування |
--rk-reel-timeline-cursor-width | 10px | Ширина капсули перемотування у спокої |
--rk-reel-timeline-cursor-width-active | 14px | Ширина капсули під час перемотування |
--rk-reel-timeline-cursor-height | 24px | Висота капсули перемотування у спокої |
--rk-reel-timeline-cursor-height-active | 32px | Висота капсули під час перемотування |
--rk-reel-timeline-hitbox | 16px | Додаткова зона влучання вказівника над доріжкою |
--rk-reel-timeline-transition | 0.15s ease-out | Анімація збільшення й зменшення доріжки та капсули |
--rk-reel-timeline-z | 11 | z-index смуги часу (над типовим шаром інтерфейсу) |
Вставте фрагмент нижче в таблицю стилів, підключену після @reelkit/react-reel-player/styles.css.
/* Brand the reel-player overlay */
:root {
--rk-reel-overlay-bg: #0f172a;
--rk-reel-button-bg: rgba(99, 102, 241, 0.65);
--rk-reel-button-bg-hover-strong: rgba(168, 85, 247, 0.85);
--rk-reel-edge-padding: 24px;
--rk-reel-button-size: 52px;
/* Timeline bar: brand-matched, beefier on desktop */
--rk-reel-timeline-track: rgba(99, 102, 241, 0.25);
--rk-reel-timeline-buffered: rgba(168, 85, 247, 0.45);
--rk-reel-timeline-fill: #a855f7;
--rk-reel-timeline-cursor: #a855f7;
--rk-reel-timeline-height: 4px;
--rk-reel-timeline-height-active: 8px;
--rk-reel-timeline-cursor-width-active: 18px;
--rk-reel-timeline-transition: 0.2s ease-out;
}Accessibility
Корінь оверлея — модальний діалог (role="dialog", aria-modal="true"). Set ariaLabel щоб змінити оголошення для екранного читача; типове значення — «Video player». Кожен слайд несе role="group", aria-roledescription="slide", and an aria-label="Слайд N з M", so swiping announces position in the sequence.
Оверлей захоплює фокус під час відкриття й повертає його на елемент-тригер після закриття. Tab і Shift+Tab циклічно проходять фокусовані елементи всередині; фокус, що вислизнув (клік поза оверлеєм, програмна установка), повертається назад. Реалізовано через captureFocusForReturn та createFocusTrap from @reelkit/core.
Клавіатурні скорочення
| Key | Action |
|---|---|
ArrowUp | Попередній слайд |
ArrowDown | Наступний слайд |
ArrowLeft | Попереднє медіа (у вкладеному слайдері) |
ArrowRight | Наступне медіа (у вкладеному слайдері) |
Escape | Закрити плеєр |