Reel Player
Eine Komponente für einen Videoplayer im Vollbild im Stil von Instagram Reels und TikTok, gebaut mit @reelkit/react-reel-player.
Funktionen
Installation
npm install @reelkit/react-reel-player @reelkit/react lucide-reactVergiss nicht, die Styles zu importieren:
import '@reelkit/react-reel-player/styles.css';Icons
Die Standard-Steuerelemente nutzen lucide-react für die Icons. Willst du eine andere Icon-Bibliothek, bring mit renderControls und renderNavigation deine eigenen mit.
Schnellstart
Die Komponente ReelPlayerOverlay rendert ein Player-Overlay im Vollbild. Übergib ein Array von ContentItem-Objekten und steuere die Sichtbarkeit mit 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>
);
}Den ganzen Ablauf, ein Raster aus Vorschaubildern, das den Player öffnet und die Zuschauer dorthin zurückbringt, wo sie aufgehört haben, zeigt das Rezept „Vom Raster zum Player “.
Anpassung
Generischer Inhaltstyp
Eigene Datentypen nutzt du, indem du BaseContentItem erweiterst:
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>
)}
/>Eigenes Slide-Overlay
Ersetze das eingebaute Slide-Overlay durch eigene Inhalte pro Slide:
<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>
)}
/>Slides ohne Medien
Mit renderSlide fügst du eigene Inhalte ein (zum Beispiel CTA-Karten). Gib null zurück, um auf die Standarddarstellung zurückzufallen:
<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;
}}
/>Eigene Steuerelemente
Setze wiederverwendbare Unterkomponenten mit deinen eigenen Ergänzungen zusammen:
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>
</>
)}
/>Eigene Timeline
Ersetze die eingebaute Wiedergabeleiste über renderTimeline durch deine eigene Oberfläche zum Spulen. Der Callback wird nur aufgerufen, wenn die Regeln des Overlays die Standardleiste rendern würden (dieselbe Logik aus dem Modus timeline und timelineMinDurationSeconds), du musst sie also nicht nachbauen. Verwende die Klasse .rk-reel-timeline auf deinem Wurzelelement wieder, um die bündige Position am unteren Rand, das Padding für den Safe Area und den Abstand zum Slide-Overlay auf Touch-Geräten zu übernehmen.
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} />
)}
/>Eigene Navigation
<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>
)}
/>Eigene verschachtelte Navigation
Ersetze die Pfeile nach links und rechts in Slides mit mehreren Medien (horizontales Karussell) durch eine eigene Navigation:
<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>
)}
/>Eigene verschachtelte Slides
Passe einzelne Slides in Karussells mit mehreren Medien mit renderNestedSlide an. Mit props.defaultContent umschließt du das Standard-ImageSlide/VideoSlide, oder du ersetzt es ganz:
// 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-State
Live-Demo ansehen →ReelPlayerUrlOverlay ist eine eigene Komponente, deren Öffnungszustand in der Adressleiste liegt. Baue mit useOverlayUrlState aus @reelkit/react einen Controller und übergib ihn als controller: Der Player öffnet sich selbst, wenn der Parameter einen Slide nennt, und schließt, wenn er verschwindet. Links lassen sich teilen, und der Zurück-Button schließt den Player, statt die Seite zu verlassen.
Eingebaute Schlüssel
Slides kannst du mit einem eingebauten Schlüssel ansprechen – spreize urlIndexKey (nach Position) oder urlStableIdKey (nach einer stabilen id) in den Controller –, beide werden aus @reelkit/react erneut exportiert. Siehe den Leitfaden zum URL-State und die Core-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} />Alle Optionen von useOverlayUrlState – param, adapter, codec, locator – stehen in der React-API-Referenz. Die Schritt-für-Schritt-Erklärung steht im React-Leitfaden.
- Das Öffnen legt einen Eintrag im Verlauf an. Das Wischen durch den Feed ersetzt ihn, N Wischgesten fügen also keine Einträge hinzu, und ein Schritt zurück verlässt immer den Player. Zurück schließt; es blättert nicht durch die Slides.
- Zurück schließt nur, wenn der Player innerhalb der App geöffnet wurde – der Link hat einen Eintrag angelegt. Ein geteilter Link, der direkt in einem neuen Tab geöffnet wird, hat keinen Verlauf hinter sich, daher verlässt Zurück im Browser die Seite; der Button ✕ oder Escape entfernt den Parameter an Ort und Stelle und bleibt.
- Ein Deep Link
?reel=3öffnet den Player beim Laden auf diesem Slide. - Ein Parameter, der keinen Slide nennt – ein veraltetes Lesezeichen, ein von Hand geänderter Wert –, wird aus der URL entfernt, statt die Adressleiste einen Slide behaupten zu lassen, der sich nicht öffnen lässt.
- Standardmäßig spricht der Parameter nur den vertikalen Beitrag an (
?reel=3). Mit einem Schlüssel für zwei Achsen trägt er auch den inneren Medienindex eines Karussells mit mehreren Medien – siehe unten.
Ein Schlüssel oder zwei: wähle die Tiefe der URL
Dasselbe ReelPlayerUrlOverlay bedient beide Formen; es unterscheidet zur Laufzeit anhand der Position des Controllers, daher gibt es kein Prop für den Modus. Den Schlüssel wählst du, wenn du den Controller baust:
| Schlüssel | Format | Trägt |
|---|---|---|
urlIndexKey(…) | ?reel=3 | Nur den vertikalen Beitrag. |
urlIndexTwoAxisKey(…) | ?reel=3.2 | Den Beitrag und den inneren Medienindex eines Karussells. |
Die beiden Formate sind bewusst verschieden – ein Schlüssel für zwei Achsen hat immer einen Punkt (3.0, nie ein einfaches 3), sodass ein einfacher Link für eine Achse nicht als der andere decodiert wird. Wechselt eine App den Schlüssel, werden deshalb alle zuvor geteilten Links ungültig. Wähle eine Form und bleib dabei.
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} />App mit Router – übergib einen Adapter. Wer am Router vorbei history.pushState schreibt, lässt dessen Location veralten, und seine nächste Navigation verliert den Parameter:
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} />Stabile Links. Der Index richtet sich nach der Position, ein gespeichertes ?reel=3 öffnet also einen anderen Beitrag, sobald der Feed umsortiert wird – für einen Feed ist das der Normalfall, nicht die Ausnahme. urlStableIdKey verwendet die stabile id jedes Beitrags als Schlüssel und durchsucht den aktuellen Feed – ein Aufruf deckt den üblichen Fall ab.
const reel = useOverlayUrlState({
param: 'reel',
...urlStableIdKey({ items: () => content }),
});Übergib hashCodec: base64UrlCodec, um die id in der URL als base64url zu codieren – eine umkehrbare Verschleierung, kein kryptografischer Hash.
Willst du ein anderes Feld als Schlüssel (einen slug) oder einen endlosen Feed mit locateAsync nachladen, baust du codec/locator selbst. Das sind zwei getrennte Aufgaben: codec schreibt die Identität in die URL, locator findet, wo diese Identität liegt.
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,
},
});Endlose Feeds. locate ist synchron und kann deshalb nur für bereits geladene Beiträge antworten – ein geteilter Link auf Beitrag 400 eines Feeds, der erst 20 geladen hat, geht ins Leere. locateAsync ist der Fallback und wird nur aufgerufen, wenn locate nichts findet.
Abkürzung
Du verwendest die id des Eintrags als Schlüssel? Spar dir den selbst gebauten codec und locator – übergib locateAsync direkt an urlStableIdKey({ items, locateAsync }) (es lädt bei einem Fehlschlag nach und gibt dann den Index zurück). Die ausführlichere Variante unten ist für ein anderes Feld als Schlüssel oder für volle Kontrolle.
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
},
},
});- Solange
locateAsyncläuft, bleibt der Player geschlossen und der Parameter unangetastet, damit der Deep Link das Laden übersteht.nulloder eine Ablehnung entfernt den Parameter. - Eine Antwort, die eintrifft, nachdem die URL sich weiterbewegt hat, nach dem Schließen oder nach dem Unmount, wird verworfen – ein langsames Laden kann keinen Slide öffnen, nach dem niemand gefragt hat.
- Während des Wartens wird nichts gerendert; den Ladezustand besitzt bereits die Seite, rendere also dein eigenes Skeleton.
- Es gibt kein Timeout – der Player kann nicht wissen, wie lang der Feed ist. Antworte mit
null, wenn die Paginierung erschöpft ist, sonst bleibt das Overlay auf unbestimmte Zeit geschlossen.
API-Referenz
Props von ReelPlayerOverlayProps
ReelPlayerOverlayProps<T>
ReelPlayerOverlayProps ist ReelPlayerOverlayBaseProps (die Props für Darstellung und Verhalten, die beide Overlays teilen) verbunden mit ReelPlayerControlledProps (isOpen und onClose). Typisiere mit dem Basistyp einen Wrapper, der eines der beiden Overlays rendert.
| Prop | Typ | Standard | Beschreibung |
|---|---|---|---|
apiRef | MutableRefObject<ReelApi> | - | Ref für den Zugriff auf die Reel-API |
ariaLabel | string | 'Video player' | Zugängliche Beschriftung der Dialog-Region; Screenreader lesen sie vor, wenn sich das Overlay öffnet |
aspectRatio | number | 9/16 (0.5625) | Verhältnis von Breite zu Höhe des Player-Containers am Desktop. Bei 768px Breite und schmaler nutzt der Player immer den ganzen Viewport. Ein Wert von null, ein negativer oder ein nicht endlicher Wert fällt auf 9/16 zurück. |
content | T[] | Pflicht | Array der Inhaltseinträge (generisch, Standard ist ContentItem) |
initialIndex | number | 0 | Index des Slides zum Start |
initialInnerIndex | number | 0 | Innerer Medienindex, bei dem geöffnet wird, nur für den anfangs sichtbaren Beitrag – damit kann eine URL mit zwei Achsen per Deep Link auf ein bestimmtes Bild eines Beitrags mit mehreren Medien zeigen. Wird ignoriert, sobald der Player offen ist und der Nutzer navigiert. |
isOpen | boolean | Pflicht | Steuert die Sichtbarkeit des Overlays. Soll die URL steuern, ob der Player offen ist, greif stattdessen zum eigenen ReelPlayerUrlOverlay – siehe URL-State oben. |
timeline | 'auto' | 'always' | 'never' | 'auto' | Strategie, wann die eingebaute Wiedergabeleiste erscheint. 'auto' rendert sie nur für Videos, die länger als timelineMinDurationSeconds sind; 'always' rendert sie immer, wenn der aktive Slide ein Video hat; 'never' schaltet die eingebaute Leiste ab (für einen komplett eigenen Ersatz nimm renderTimeline). |
timelineMinDurationSeconds | number | 30 | Mindestdauer des Videos (Sekunden), ab der timeline='auto' die eingebaute Leiste rendert. Kurze Clips in Schleife unter dieser Schwelle bekommen keine. |
renderControls | (props: ControlsRenderProps) => ReactNode | - | Eigene Steuerelemente, ersetzt die Standard-Buttons zum Schließen und für den Ton |
renderError | (props: { item: T; activeIndex: number }) => ReactNode | - | Eigene Fehleranzeige, ersetzt das Standard-Fehlericon |
renderLoading | (props: { item: T; activeIndex: number }) => ReactNode | - | Eigene Ladeanzeige, ersetzt den Standard-Wellenlader |
renderNavigation | (props: NavigationRenderProps) => ReactNode | - | Eigene Navigation, ersetzt die Standard-Pfeile nach oben und unten |
renderNestedNavigation | (props: NavigationRenderProps) => ReactNode | - | Eigene Navigation für den verschachtelten horizontalen Slider (Beiträge mit mehreren Medien), ersetzt die Standard-Pfeile nach links und rechts |
renderNestedSlide | (props: NestedSlideRenderProps) => ReactNode | - | Eigener Renderer für die Einträge des verschachtelten horizontalen Sliders. Mit props.defaultContent umschließt oder bettest du das Standard-ImageSlide/VideoSlide ein. Anders als bei renderSlide gilt null hier nicht als Rückfall. |
renderSlide | (props: SlideRenderProps) => ReactNode | null | - | Eigenes Rendern der Slides. Gib null zurück, um auf die Standarddarstellung zurückzufallen. Mit props.defaultContent umschließt oder bettest du den Standard-Slide ein. |
renderSlideOverlay | (item, index, isActive) => ReactNode | - | Eigenes Overlay pro Slide, ersetzt das Standard-SlideOverlay. Gib null zurück, um es auszublenden. |
renderTimeline | (props: TimelineRenderProps) => ReactNode | - | Eigene Wiedergabeleiste. Wird nur aufgerufen, wenn die Regeln die Standardleiste rendern würden (dieselbe Logik aus auto/always/never und timelineMinDurationSeconds). Mit props.defaultContent umschließt du das eingebaute <TimelineBar />; gib null zurück, um sie auszublenden. |
Props von ReelPlayerUrlOverlay
ReelPlayerUrlOverlayProps<T>
Nimmt jedes Prop für Darstellung und Verhalten von oben außer isOpen, das durch einen controller ersetzt wird: Der Typ ist ReelPlayerOverlayBaseProps verbunden mit ReelPlayerUrlControlledProps. initialIndex wird ignoriert – die Position des Controllers bestimmt den Slide, ein zusätzlich übergebener Wert wird also bei jedem Öffnen überschrieben.
| Prop | Typ | Standard | Beschreibung |
|---|---|---|---|
controller | UrlStateController | required | Controller aus useOverlayUrlState. Seine Position entscheidet, ob das Overlay offen ist und welchen Slide es zeigt; beim Slide-Wechsel und beim Schließen schreibt das Overlay über ihn zurück. |
Callbacks
| Prop | Typ | Beschreibung |
|---|---|---|
onClose | () => void | Wird aufgerufen, wenn der Player schließt. Pflicht bei ReelPlayerOverlay (du steuerst, ob er offen ist, und musst das Schließen also selbst behandeln); optional bei ReelPlayerUrlOverlay, wo die URL das Schließen steuert – übergib ihn nur, um nach dem Schließen zu reagieren. |
onSlideChange | (index: number) => void | Wird nach dem Slide-Wechsel aufgerufen |
onInnerSlideChange | (outerIndex: number, innerIndex: number) => void | Wird aufgerufen, wenn sich der innere Medienindex des aktiven Beitrags ändert – bei der inneren Navigation in einem Beitrag mit mehreren Medien und beim Aktivieren eines äußeren Beitrags, dann mit dessen aktuellem inneren Index (0 für einen Beitrag mit nur einem Medium). |
Props von Reel (durchgereicht)
Diese Props werden an die darunterliegende Komponente Reel weitergegeben. Ihr Typ ist ReelProxyProps, die Teilmenge der Props von Reel, die das Overlay durchreicht; typisiere damit einen Wrapper, der sie weitergibt.
| Prop | Typ | Standard | Beschreibung |
|---|---|---|---|
enableNavKeys | boolean | true | Schaltet die Tastaturnavigation ein |
enableWheel | boolean | true | Schaltet die Navigation mit dem Mausrad ein |
loop | boolean | false | Schaltet die Endlosschleife ein |
swipeDistanceFactor | number | 0.12 | Schwelle für das Wischen (0-1) |
transitionDuration | number | 300 | Dauer der Übergangsanimation (ms) |
wheelDebounceMs | number | 200 | Dauer des Debounce für das Mausrad (ms) |
Typen
BaseContentItem
Der Typ für die generische Einschränkung. Erweitere ihn, um eigene Datentypen mit ReelPlayerOverlay zu nutzen.
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;
}Unterkomponenten
Wiederverwendbare Bausteine, exportiert zum Zusammensetzen in eigenen Render-Props:
CloseButton
Eigenständiger Button zum Schließen im Standardstil des Players. Zur Verwendung in renderControls.
import { CloseButton } from '@reelkit/react-reel-player';
<CloseButton onClick={onClose} />
<CloseButton onClick={onClose} className="my-close-btn" style={{ top: 24, right: 24 }} />SoundButton
Eigenständiger Schalter für den Ton. Muss in einem SoundProvider liegen (den ReelPlayerOverlay automatisch bereitstellt).
import { SoundButton } from '@reelkit/react-reel-player';
<SoundButton />
<SoundButton disabled className="my-sound-btn" />TimelineBar
Standardleiste zum Spulen der Wiedergabe. Liest aus dem nächsten TimelineProvider (der in ReelPlayerOverlay automatisch gemountet ist) und rendert die Spur, die gepufferten Bereiche, die Fortschrittsfüllung und den Griff zum Spulen. Gestalte sie über die Custom Properties --rk-reel-timeline-*, oder ersetze sie über 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
Das Standard-Overlay mit Verlauf, das Autor, Beschreibung und Likes zeigt. Wird automatisch gerendert, wenn der Inhalt die nötigen Felder hat. Mit renderSlideOverlay ersetzt oder versteckst du es.
import { SlideOverlay } from '@reelkit/react-reel-player';
<SlideOverlay
author={{ name: 'John', avatar: '/avatar.jpg' }}
description="Amazing content"
likes={12500}
/>ImageSlide
Bild-Slide mit Lazy Loading und standardmäßig object-fit: cover. Zur Verwendung in renderSlide, um eigene Bild-Slides mit deinen eigenen Styles zusammenzusetzen.
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-Slide mit gemeinsamem <video>-Element für durchgehenden Ton auf iOS, Poster-Frames, Positionsgedächtnis und Ladeanzeige. Muss in einem SoundProvider liegen (den ReelPlayerOverlay automatisch bereitstellt).
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 }}
/>Eigene Slides zusammensetzen
Nutze renderSlide mit ImageSlide / VideoSlide, um das Rendern der Medien anzupassen und dabei das ganze eingebaute Verhalten zu behalten (Autoplay, Poster-Erfassung, Ton-Synchronisierung).
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
}}
/>Laden von Inhalten und Fehlerbehandlung
Der Player verfolgt Lade- und Fehlerzustände pro Slide. Während Inhalte laden, erscheint ein Wellenlader; bei defekten Medien ein Fehlericon. Fehlerhafte URLs werden zwischengespeichert, sodass ein erneuter Besuch den Fehler sofort zeigt, ohne es noch einmal zu versuchen.
Callbacks für den Lebenszyklus
Wenn du renderSlide nutzt, rufst du diese Callbacks auf, um die Ladeanzeige zu steuern:
| Callback | Wann aufrufen |
|---|---|
onReady | Das Bild ist geladen oder das Video spielt. Setzt Lade- und Fehlerzustand zurück. |
onWaiting | Das Video puffert mitten in der Wiedergabe. Zeigt die Ladeanzeige. |
onError | Der Inhalt ließ sich nicht laden. Zeigt das Fehler-Overlay und merkt sich die URL als defekt. |
// 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>
)}Eigene Oberfläche für Laden und Fehler
Ersetze den Standard-Wellenlader und das Fehlericon durch eigene Komponenten:
<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
Das Overlay rendert eine eingebaute Wiedergabeleiste über dem aktiven Video. Das Prop timeline vom Typ TimelineMode steuert, wann sie erscheint:
'auto'(Standard): rendert, wenn das aktive Medium ein Video ist, dessen DauertimelineMinDurationSecondsüberschreitet (Standard 30). Funktioniert für Slides mit einem einzelnen Video und für Karussells mit mehreren Medien; die Leiste folgt dem aktiven verschachtelten Eintrag und verschwindet bei Bildern.'always': rendert immer, wenn der aktive Slide ein Video hat.'never': rendert nie. Eine eigene Leiste baust du überrenderTimeline.
<ReelPlayerOverlay
isOpen={isOpen}
onClose={close}
content={items}
timeline="auto"
timelineMinDurationSeconds={30}
/>Gestalte sie über die CSS Custom Properties --rk-reel-timeline-* (Höhe, Farben, Größe des Griffs). Für eine komplett eigene Leiste zum Spulen, einen Timecode oder eine Fortschrittsanzeige nutzt du renderTimeline; der Callback bekommt einen timelineState, hinter dem der zugrunde liegende TimelineController steht.
Eine Komponente, die im Overlay gerendert wird, kann denselben Controller selbst lesen: useTimelineState() holt ihn aus dem nächsten TimelineProvider (den das Overlay mountet) und wirft außerhalb davon einen Fehler; useTimelineStateOptional() gibt dort stattdessen null zurück, für Komponenten, die auch außerhalb des Players gerendert werden.
Ton-Context
Für eigene Implementierungen kannst du auf den Zustand des Tons zugreifen:
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-Klassen
Alle CSS-Klassen sind einfache Klassen (keine CSS Modules), du kannst sie also mit spezifischeren Selektoren in einem Stylesheet ansprechen, das nach @reelkit/react-reel-player/styles.css geladen wird. Für Änderungen an Farbe, Größe und z-index nimm lieber die CSS Custom Properties aus dem Abschnitt Theming unten – genau dafür sind sie gemacht.
| Klasse | Komponente | Beschreibung |
|---|---|---|
.rk-reel-overlay | Overlay | Fester Hintergrund im Vollbild (Hintergrund, z-index) |
.rk-reel-container | Overlay | Container des Players (Position, Overflow) |
.rk-reel-loader | Overlay | Overlay mit der Wellen-Ladeanimation |
.rk-reel-media-error | Overlay | Overlay für den Fehlerzustand (zentriertes Icon + Text) |
.rk-reel-media-error-text | Overlay | Text der Fehlermeldung |
.rk-reel-button | Controls | Gemeinsamer runder Icon-Button (Schließen, Ton, Navigationspfeile) |
.rk-reel-close-btn | Controls | Button zum Schließen |
.rk-reel-sound-btn | Controls | Button zum Umschalten des Tons |
.rk-reel-nav-arrows | Navigation | Container der Pfeile, nur am Desktop (versteckt bei 768px Breite und schmaler) |
.rk-reel-nav-button | Navigation | Einzelner Navigationspfeil für vorher/weiter |
.rk-reel-slide-wrapper | Slide | Hülle um Medium + Overlay |
.rk-reel-slide-overlay | SlideOverlay | Container des Overlays mit Verlauf |
.rk-reel-slide-overlay-author | SlideOverlay | Zeile mit dem Autor (Avatar + Name) |
.rk-reel-slide-overlay-avatar | SlideOverlay | Avatarbild des Autors |
.rk-reel-slide-overlay-name | SlideOverlay | Text mit dem Namen des Autors |
.rk-reel-slide-overlay-description | SlideOverlay | Text der Beschreibung |
.rk-reel-slide-overlay-likes | SlideOverlay | Zeile mit den Likes (Herz + Anzahl) |
.rk-reel-video-container | VideoSlide | Hülle des Videos (Hintergrund, Overflow) |
.rk-reel-video-element | VideoSlide | Das Element <video> |
.rk-reel-video-poster | VideoSlide | Posterbild (blendet bei der Wiedergabe aus) |
.rk-reel-video-poster.rk-visible | VideoSlide | Zustands-Modifier auf dem Poster, solange das Video pausiert oder lädt |
.rk-reel-nested-indicator | NestedSlider | Punkt-Paginierung unter Slides mit mehreren Medien (Position je nach Desktop oder Touch) |
.rk-reel-nested-nav | NestedSlider | Pfeile des horizontalen Karussells (versteckt bei 768px Breite und schmaler) |
.rk-reel-nested-nav-next | NestedSlider | Position des verschachtelten Pfeils weiter |
.rk-reel-nested-nav-prev | NestedSlider | Position des verschachtelten Pfeils zurück |
.rk-reel-timeline | TimelineBar | Hülle der Leiste zum Spulen. Auf den Wurzelelementen eines eigenen `renderTimeline` wiederverwenden, um die bündige Position am unteren Rand, das Padding für den Safe Area und den Abstand zum Slide-Overlay auf Touch-Geräten zu übernehmen. |
.rk-reel-timeline-track | TimelineBar | Spur (noch nicht gespielter Bereich) |
.rk-reel-timeline-buffered | TimelineBar | Ebene der gepufferten Abschnitte |
.rk-reel-timeline-fill | TimelineBar | Füllung für den gespielten Fortschritt |
.rk-reel-timeline-cursor | TimelineBar | Griff zum Spulen (schwebt über der Spur) |
Theming
Jede Farbe, Größe, jeder z-index und jeder Übergang liegt in einer CSS Custom Property. Überschreibe eine oder mehrere auf :root (oder einem beliebigen Vorfahren des Overlays), um das Theme zu ändern, ohne den Quellcode der Komponente anzufassen.
| Token | Standard | Steuert |
|---|---|---|
--rk-reel-overlay-bg | #000 | Farbe des Hintergrunds im Vollbild |
--rk-reel-overlay-z | 1000 | z-index des Overlays |
--rk-reel-button-bg | rgba(0, 0, 0, 0.5) | Hintergrund des runden Standard-Buttons |
--rk-reel-button-bg-hover | rgba(255, 255, 255, 0.1) | Hintergrund der Navigationspfeile (und Basis für den Hover-Zustand) |
--rk-reel-button-bg-hover-strong | rgba(255, 255, 255, 0.2) | Hintergrund der Navigationspfeile beim Hover |
--rk-reel-button-fg | #fff | Farbe der Button-Icons |
--rk-reel-button-size | 44px | Breite / Höhe der Buttons |
--rk-reel-button-radius | 50% | border-radius der Buttons |
--rk-reel-ui-z | 10 | z-index von Schließen / Ton / Navigation |
--rk-reel-edge-padding | 16px | Abstand zum Rand für Schließen / Ton / Navigationspfeile |
--rk-reel-nav-gap | 8px | Abstand zwischen den übereinanderliegenden Navigationspfeilen |
--rk-reel-transition | 0.2s | Dauer des Hover-Übergangs |
--rk-reel-loader-color | rgba(255, 255, 255, 0.12) | Farbe des Verlaufs im Wellenlader |
--rk-reel-loader-duration | 1.8s | Dauer der Animation des Wellenladers |
--rk-reel-error-fg | rgba(255, 255, 255, 0.4) | Farbe von Fehlericon und -text |
--rk-reel-error-text-size | 13px | Schriftgröße der Fehlermeldung |
--rk-reel-slide-overlay-bg | linear-gradient(transparent, rgba(0, 0, 0, 0.7)) | Verlauf hinter der Bildunterschrift |
--rk-reel-slide-overlay-padding | 48px 16px 16px | Innenabstand der Bildunterschrift |
--rk-reel-slide-overlay-name-color | #fff | Farbe des Autorennamens |
--rk-reel-slide-overlay-description-color | rgba(255, 255, 255, 0.9) | Farbe des Beschreibungstexts |
--rk-reel-slide-overlay-likes-color | rgba(255, 255, 255, 0.8) | Textfarbe der Zeile mit den Likes |
--rk-reel-video-bg | #000 | Hintergrund der Letterbox hinter <video> |
--rk-reel-nested-button-bg | rgba(0, 0, 0, 0.5) | Hintergrund der verschachtelten Pfeile |
--rk-reel-nested-button-bg-hover | rgba(255, 255, 255, 0.2) | Hintergrund der verschachtelten Pfeile beim Hover |
--rk-reel-nested-button-size | 36px | Größe der verschachtelten Pfeile |
--rk-reel-nested-edge-padding | 12px | Abstand der verschachtelten Pfeile zum Rand |
--rk-reel-timeline-track | rgba(255, 255, 255, 0.22) | Hintergrund der Spur (noch nicht gespielter Bereich) |
--rk-reel-timeline-buffered | rgba(255, 255, 255, 0.4) | Farbe der gepufferten Abschnitte |
--rk-reel-timeline-fill | #fff | Farbe der Füllung für den gespielten Fortschritt |
--rk-reel-timeline-cursor | #fff | Farbe des Griffs zum Spulen |
--rk-reel-timeline-height | 3px | Höhe der Spur in Ruhe |
--rk-reel-timeline-height-active | 6px | Höhe der Spur bei Hover / Fokus / Spulen |
--rk-reel-timeline-cursor-width | 10px | Breite des Griffs in Ruhe |
--rk-reel-timeline-cursor-width-active | 14px | Breite des Griffs beim Spulen |
--rk-reel-timeline-cursor-height | 24px | Höhe des Griffs in Ruhe |
--rk-reel-timeline-cursor-height-active | 32px | Höhe des Griffs beim Spulen |
--rk-reel-timeline-hitbox | 16px | Zusätzliche Trefferfläche für den Zeiger über der Spur |
--rk-reel-timeline-transition | 0.15s ease-out | Animation für das Wachsen und Schrumpfen von Spur und Griff |
--rk-reel-timeline-z | 11 | z-index der Timeline (über der Standardebene der UI) |
Füge das Snippet unten in ein Stylesheet ein, das nach @reelkit/react-reel-player/styles.css geladen wird.
/* 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;
}Barrierefreiheit
Das Wurzelelement des Overlays ist ein modaler Dialog (role="dialog", aria-modal="true"). Mit ariaLabel änderst du die Ansage für Screenreader; standardmäßig lautet sie "Video player". Jeder Slide trägt role="group", aria-roledescription="slide" und ein aria-label="Slide N of M", sodass beim Wischen die Position in der Abfolge angesagt wird.
Das Overlay fängt beim Öffnen den Fokus ein und gibt ihn beim Schließen an den Auslöser zurück. Tab und Shift+Tab wandern durch die fokussierbaren Elemente darin; Fokus, der entkommt (Klick nach außen, Fokus aus dem Code), wird zurückgeholt. Umgesetzt mit captureFocusForReturn und createFocusTrap aus @reelkit/core.
Tastenkürzel
| Taste | Aktion |
|---|---|
ArrowUp | Vorheriger Slide |
ArrowDown | Nächster Slide |
ArrowLeft | Vorheriges Medium (im verschachtelten Slider) |
ArrowRight | Nächstes Medium (im verschachtelten Slider) |
Escape | Player schließen |