Reel Player

Повноекранний компонент відеоплеєра у стилі Instagram Reels чи TikTok на основі @reelkit/react-reel-player.

Подивитися демо наживо →

Features

Вертикальний свайп
Дотик, перетягування, клавіатура, колесо
Автовідтворення відео
Грає, коли слайд видимий
Перемикач звуку
Безперервність на iOS
Multi-Media
Вкладені горизонтальні каруселі
Пам’ять позиції
Продовжує з місця, де зупинилися
Знімок кадру
Плавний перехід від постера до відео
Віртуалізований
Лише 3 слайди в DOM
Співвідношення сторін
9:16 на десктопі, на весь екран на мобільних
Навігація на десктопі
Кнопки-стрілки
Узагальнені типи
Власні моделі даних вмісту
Customizable
Render props для всього
Оверлей слайда
Автор, лайки, опис
Стан в URL
Посилання, якими можна ділитися й зберігати в закладки

Встановлення

bash
npm install @reelkit/react-reel-player @reelkit/react lucide-react

Не забудьте імпортувати стилі:

typescript
import '@reelkit/react-reel-player/styles.css';
Icons

Стандартні елементи керування використовують lucide-react для іконок. Якщо ви віддаєте перевагу іншій бібліотеці іконок, скористайтеся renderControls та renderNavigation , щоб передати власні.

Швидкий старт

The ReelPlayerOverlay рендерить повноекранний оверлей плеєра. Передайте масив ContentItem і керуйте видимістю через isOpen.

tsx
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}
      />
    </>
  );
}
ReelPlayerPage.tsx
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:

tsx
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>
  )}
/>

Власний оверлей слайда

Замініть вбудований оверлей слайда власним вмістом для кожного слайда:

tsx
<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 щоб лишити стандартний:

tsx
<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;
  }}
/>

Власні елементи керування

Складайте готові підкомпоненти разом із власними доповненнями:

tsx
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 на своєму корені, щоб успадкувати притиснення до низу, відступи безпечної зони та проміжок для оверлея слайда на дотикових пристроях.

tsx
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} />
  )}
/>

Власна навігація

tsx
<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>
  )}
/>

Власна вкладена навігація

Замініть стрілки ліворуч і праворуч усередині мультимедійних слайдів (горизонтальна карусель) власною навігацією:

tsx
<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 або замінити його повністю:

tsx
// 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 ядра.

tsx
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 контролера, тож жодного пропса режиму немає. Ключ обирайте, коли будуєте контролер:

KeyWireCarries
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.

tsx
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 повз роутер лишає його місцеположення застарілим, і наступна навігація втрачає параметр:

tsx
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.

tsx
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 знаходить, де ця ідентичність лежить.

tsx
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 }) (він вантажить дані, якщо не знайшов, і повертає індекс). Розгорнутий варіант нижче — для адресації за іншим полем або для повного контролю.

tsx
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ТипТипове значенняОпис
apiRefMutableRefObject<ReelApi>-Ref для доступу до API Reel
ariaLabelstring'Video player'Доступна назва області діалогу; екранні читачі оголошують її, коли оверлей відкривається
aspectRationumber9/16 (0.5625)Співвідношення ширини до висоти контейнера плеєра на десктопі. На ширині 768px і вужче плеєр завжди займає всю область перегляду. Нульове, від’ємне чи нескінченне значення замінюється на 9/16.
contentT[]requiredМасив елементів вмісту (узагальнений тип, типово ContentItem)
initialIndexnumber0Початковий індекс слайда
initialInnerIndexnumber0Індекс внутрішнього медіа, з якого відкриватися, лише для початково видимого допису — дає змогу двовісному URL вести просто до потрібного зображення мультимедійного допису. Ігнорується, щойно плеєр відкрито й користувач починає гортати.
isOpenbooleanrequiredКерує видимістю оверлея. Для стану відкриття, керованого URL, беріть окремий ReelPlayerUrlOverlay — дивіться розділ про стан в URL нижче.
timeline'auto' | 'always' | 'never''auto'Правило показу вбудованої смуги таймлайну. 'auto' показує її лише для відео, довших за timelineMinDurationSeconds; 'always' — щойно на активному слайді є відео; 'never' вимикає вбудовану смугу (для повної заміни беріть renderTimeline).
timelineMinDurationSecondsnumber30Мінімальна тривалість відео (у секундах), за якої 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ТипТипове значенняОпис
controllerUrlStateControllerrequiredКонтролер із useOverlayUrlState. Його position вирішує, чи оверлей відкритий і який слайд показує; оверлей записує через нього назад на зміну слайда та на закриття.

Колбеки

PropТипОпис
onClose() => voidВикликається, коли плеєр закривається. Обов’язковий у ReelPlayerOverlay (стан відкриття ваш, тож закриття обробляєте ви); необов’язковий у ReelPlayerUrlOverlay, де закриттям керує URL — передавайте лише щоб зреагувати після закриття.
onSlideChange(index: number) => voidВикликається після зміни слайда
onInnerSlideChange(outerIndex: number, innerIndex: number) => voidВикликається, коли змінюється індекс внутрішнього медіа активного допису — під час навігації всередині мультимедійного допису й під час активації зовнішнього, повідомляючи поточний внутрішній індекс активованого допису (0 для допису з одним медіа).

Пропси Reel (передані далі)

Ці пропси передаються далі в компонент Reel. Їхній тип — ReelProxyProps, підмножина пропсів Reel, яку оверлей передає далі; типізуйте ним обгортку, що їх прокидає.

PropТипТипове значенняОпис
enableNavKeysbooleantrueВмикає навігацію з клавіатури
enableWheelbooleantrueВмикає навігацію колесом миші
loopbooleanfalseВмикає нескінченний цикл
swipeDistanceFactornumber0.12Поріг свайпу (0–1)
transitionDurationnumber300Тривалість анімації переходу (мс)
wheelDebounceMsnumber200Тривалість дебаунсу колеса (мс)

Types

BaseContentItem

Тип-обмеження для узагальнення. Розширюйте його, щоб використовувати власні типи даних із ReelPlayerOverlay.

typescript
interface BaseContentItem {
  id: string;
  media: MediaItem[];
}

ContentItem

typescript
interface ContentItem extends BaseContentItem {
  author: {
    name: string;
    avatar: string;
  };
  likes: number;
  description: string;
}

MediaItem

typescript
interface MediaItem {
  id: string;
  type: 'image' | 'video';
  src: string;
  poster?: string;
  aspectRatio: number;
}

MediaType

typescript
type MediaType = 'image' | 'video';

ControlsRenderProps<T>

typescript
interface ControlsRenderProps<T extends BaseContentItem> {
  onClose: () => void;
  soundState: SoundState;
  activeIndex: number;
  content: T[];
}
typescript
interface NavigationRenderProps {
  onPrev: () => void;
  onNext: () => void;
  activeIndex: number;
  count: number;
}

SlideRenderProps<T>

typescript
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

typescript
interface NestedSlideRenderProps {
  item: MediaItem;
  index: number;
  size: [number, number];
  isActive: boolean;
  isInnerActive: boolean;
  slideKey: string;
  onVideoRef?: (ref: HTMLVideoElement | null) => void;
  defaultContent: ReactNode;
}

SlideOverlayProps

typescript
interface SlideOverlayProps {
  author?: { name: string; avatar: string };
  description?: string;
  likes?: number;
}

ImageSlideProps

typescript
interface ImageSlideProps {
  src: string;
  size: [number, number];
  className?: string;
  style?: CSSProperties;
  imgClassName?: string;
  imgStyle?: CSSProperties;
}

VideoSlideProps

typescript
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

typescript
interface CloseButtonProps {
  onClick: () => void;
  className?: string;
  style?: React.CSSProperties;
}

SoundButtonProps

typescript
interface SoundButtonProps {
  disabled?: boolean;
  className?: string;
  style?: React.CSSProperties;
}

TimelineBarProps

typescript
interface TimelineBarProps {
  className?: string;
  style?: React.CSSProperties;
}

TimelineRenderProps<T>

typescript
interface TimelineRenderProps<T extends BaseContentItem> {
  item: T;
  activeIndex: number;
  timelineState: TimelineController;
  defaultContent: ReactNode;
}

Sub-Components

Повторно використовувані блоки, експортовані для складання власних render props:

CloseButton

Окрема кнопка закриття зі стандартними стилями плеєра. Використовуйте всередині renderControls.

tsx
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 додає його автоматично).

tsx
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.

tsx
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 його можна замінити або сховати.

tsx
import { SlideOverlay } from '@reelkit/react-reel-player';

<SlideOverlay
  author={{ name: 'John', avatar: '/avatar.jpg' }}
  description="Amazing content"
  likes={12500}
/>

ImageSlide

Слайд-зображення з лінивим завантаженням і object-fit: cover за замовчуванням. Використовуйте всередині renderSlide щоб складати власні слайди-зображення зі своїми стилями.

tsx
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 додає його автоматично).

tsx
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 щоб змінити рендеринг медіа, зберігши всю вбудовану поведінку (автовідтворення, знімок постера, синхронізацію звуку).

tsx
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 як зіпсований.
tsx
// 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>
)}

Власний інтерфейс завантаження та помилок

Замініть стандартний хвильовий індикатор і значок помилки власними компонентами:

tsx
<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.
tsx
<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 — для компонентів, які можуть рендеритися й поза плеєром.

Контекст звуку

Для власних реалізацій доступ до стану звуку відкритий:

tsx
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 нижче — вони саме для цього й зроблені.

ClassComponentОпис
.rk-reel-overlayOverlayФіксоване повноекранне тло (фон, z-index)
.rk-reel-containerOverlayКонтейнер плеєра (позиція, переповнення)
.rk-reel-loaderOverlayОверлей хвильової анімації завантаження
.rk-reel-media-errorOverlayОверлей стану помилки (значок і текст по центру)
.rk-reel-media-error-textOverlayТекст повідомлення про помилку
.rk-reel-buttonControlsСпільна кругла кнопка з іконкою (закриття, звук, стрілки)
.rk-reel-close-btnControlsКнопка закриття
.rk-reel-sound-btnControlsКнопка перемикання звуку
.rk-reel-nav-arrowsНавігаціяКонтейнер стрілок лише для десктопа (прихований на ширині 768px і вужче)
.rk-reel-nav-buttonНавігаціяОкрема стрілка вперед або назад
.rk-reel-slide-wrapperSlideОбгортка навколо медіа та оверлея
.rk-reel-slide-overlaySlideOverlayКонтейнер градієнтного оверлея
.rk-reel-slide-overlay-authorSlideOverlayРядок автора (аватар та ім’я)
.rk-reel-slide-overlay-avatarSlideOverlayЗображення аватара автора
.rk-reel-slide-overlay-nameSlideOverlayТекст імені автора
.rk-reel-slide-overlay-descriptionSlideOverlayТекст опису
.rk-reel-slide-overlay-likesSlideOverlayРядок лайків (сердечко й лічильник)
.rk-reel-video-containerVideoSlideОбгортка відео (фон, переповнення)
.rk-reel-video-elementVideoSlideЕлемент <video>
.rk-reel-video-posterVideoSlideПостер (зникає з початком відтворення)
.rk-reel-video-poster.rk-visibleVideoSlideМодифікатор стану постера, поки відео на паузі або вантажиться
.rk-reel-nested-indicatorNestedSliderПагінація точками під мультимедійними слайдами (позиція різна на десктопі й на дотикових пристроях)
.rk-reel-nested-navNestedSliderСтрілки горизонтальної каруселі (приховані на ширині 768px і вужче)
.rk-reel-nested-nav-nextNestedSliderПозиція вкладеної стрілки вперед
.rk-reel-nested-nav-prevNestedSliderПозиція вкладеної стрілки назад
.rk-reel-timelineTimelineBarОбгортка смуги перемотування. Використовуйте на власних коренях `renderTimeline`, щоб успадкувати притиснення до низу, відступи безпечної зони та проміжок для оверлея слайда на дотикових пристроях.
.rk-reel-timeline-trackTimelineBarДоріжка (невідтворена частина)
.rk-reel-timeline-bufferedTimelineBarШар буферизованих сегментів
.rk-reel-timeline-fillTimelineBarЗаповнення відтвореного прогресу
.rk-reel-timeline-cursorTimelineBarПовзунок перемотування (плаває над доріжкою)

Theming

Кожен колір, розмір, z-index і перехід живе у власній властивості CSS. Перевизначайте одну чи кілька на :root (або на будь-якому предку оверлея), щоб змінити тему, не чіпаючи код компонентів.

TokenТипове значенняControls
--rk-reel-overlay-bg#000Колір повноекранного фону
--rk-reel-overlay-z1000z-index оверлею
--rk-reel-button-bgrgba(0, 0, 0, 0.5)Типовий фон круглої кнопки
--rk-reel-button-bg-hoverrgba(255, 255, 255, 0.1)Фон стрілки навігації (і базовий стан наведення)
--rk-reel-button-bg-hover-strongrgba(255, 255, 255, 0.2)Фон стрілки навігації під час наведення
--rk-reel-button-fg#fffКолір іконки кнопки
--rk-reel-button-size44pxШирина й висота кнопки
--rk-reel-button-radius50%Радіус заокруглення кнопки
--rk-reel-ui-z10z-index кнопок закриття, звуку й навігації
--rk-reel-edge-padding16pxВідступ від краю для кнопок закриття, звуку та стрілок навігації
--rk-reel-nav-gap8pxВідстань між розташованими одна над одною стрілками навігації
--rk-reel-transition0.2sТривалість переходу під час наведення
--rk-reel-loader-colorrgba(255, 255, 255, 0.12)Колір градієнта хвильового завантажувача
--rk-reel-loader-duration1.8sТривалість анімації хвильового завантажувача
--rk-reel-error-fgrgba(255, 255, 255, 0.4)Колір іконки й тексту помилки
--rk-reel-error-text-size13pxРозмір шрифту повідомлення про помилку
--rk-reel-slide-overlay-bglinear-gradient(transparent, rgba(0, 0, 0, 0.7))Градієнт затемнення під підписом
--rk-reel-slide-overlay-padding48px 16px 16pxВнутрішній відступ підпису
--rk-reel-slide-overlay-name-color#fffКолір імені автора
--rk-reel-slide-overlay-description-colorrgba(255, 255, 255, 0.9)Колір тексту опису
--rk-reel-slide-overlay-likes-colorrgba(255, 255, 255, 0.8)Колір тексту рядка вподобань
--rk-reel-video-bg#000Фон смуг навколо <video>
--rk-reel-nested-button-bgrgba(0, 0, 0, 0.5)Фон вкладеної стрілки
--rk-reel-nested-button-bg-hoverrgba(255, 255, 255, 0.2)Фон вкладеної стрілки під час наведення
--rk-reel-nested-button-size36pxРозмір вкладеної стрілки
--rk-reel-nested-edge-padding12pxВідступ вкладеної стрілки від краю
--rk-reel-timeline-trackrgba(255, 255, 255, 0.22)Фон доріжки (невідтворена частина)
--rk-reel-timeline-bufferedrgba(255, 255, 255, 0.4)Колір буферизованих відрізків
--rk-reel-timeline-fill#fffКолір заповнення відтвореної частини
--rk-reel-timeline-cursor#fffКолір капсули повзунка перемотування
--rk-reel-timeline-height3pxВисота доріжки у спокої
--rk-reel-timeline-height-active6pxВисота доріжки під час наведення, фокусу й перемотування
--rk-reel-timeline-cursor-width10pxШирина капсули перемотування у спокої
--rk-reel-timeline-cursor-width-active14pxШирина капсули під час перемотування
--rk-reel-timeline-cursor-height24pxВисота капсули перемотування у спокої
--rk-reel-timeline-cursor-height-active32pxВисота капсули під час перемотування
--rk-reel-timeline-hitbox16pxДодаткова зона влучання вказівника над доріжкою
--rk-reel-timeline-transition0.15s ease-outАнімація збільшення й зменшення доріжки та капсули
--rk-reel-timeline-z11z-index смуги часу (над типовим шаром інтерфейсу)

Вставте фрагмент нижче в таблицю стилів, підключену після @reelkit/react-reel-player/styles.css.

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.

Клавіатурні скорочення

KeyAction
ArrowUpПопередній слайд
ArrowDownНаступний слайд
ArrowLeftПопереднє медіа (у вкладеному слайдері)
ArrowRightНаступне медіа (у вкладеному слайдері)
EscapeЗакрити плеєр