Reel Player
A full-screen Instagram-Reel/TikTok-style video player component using @reelkit/react-reel-player.
Features
Installation
Don't forget to import the styles:
Icons
lucide-react for icons. If you prefer a different icon library, use renderControls and renderNavigation to provide your own.Quick Start
The ReelPlayerOverlay component renders a full-screen player overlay. Pass an array of ContentItem objects and control visibility with isOpen.
Live Demo
Click a thumbnail to open the full-screen player. Press Escape or the close button to return.
Customization
Generic Content Type
Use custom data types by extending BaseContentItem:
Custom Slide Overlay
Replace the built-in slide overlay with custom content per slide:
Non-Media Slides
Use renderSlide to inject custom content (e.g., CTA cards). Return null to fall back to default:
Custom Controls
Compose reusable sub-components with your own additions:
Custom Timeline
Replace the built-in playback bar with your own scrub UI via renderTimeline. The callback only fires when the overlay's gating rules would render the default bar (same timeline mode + timelineMinDurationSeconds logic), so you don't re-implement it. Reuse the .rk-reel-timeline class on your root to inherit flush-bottom positioning, safe-area padding, and touch-device slide-overlay clearance.
Custom Navigation
Custom Nested Navigation
Replace the left/right arrows inside multi-media slides (horizontal carousel) with custom navigation:
Custom Nested Slides
Customise individual slides inside multi-media carousels with renderNestedSlide. Use props.defaultContent to wrap the default ImageSlide/VideoSlide, or replace it entirely:
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| apiRef | MutableRefObject<ReelApi> | - | Ref to access Reel API |
| ariaLabel | string | 'Video player' | Accessible label for the dialog region; announced by screen readers when the overlay opens |
| aspectRatio | number | 9/16 (0.5625) | Width/height ratio for the player container on desktop. On mobile the player always uses full viewport. |
| content | T[] | required | Array of content items (generic, defaults to ContentItem) |
| initialIndex | number | 0 | Starting slide index |
| isOpen | boolean | required | Controls overlay visibility |
| timeline | 'auto' | 'always' | 'never' | 'auto' | Gating strategy for the built-in playback timeline bar. 'auto' renders only for videos longer than timelineMinDurationSeconds; 'always' renders whenever the active slide has a video; 'never' disables the built-in bar (use renderTimeline for a fully custom replacement). |
| timelineMinDurationSeconds | number | 30 | Minimum video duration (seconds) for timeline='auto' to render the built-in bar. Short looping clips below this threshold are suppressed. |
| renderControls | (props: ControlsRenderProps) => ReactNode | - | Custom controls, replaces default close + sound buttons |
| renderError | (props: { item: T; activeIndex: number }) => ReactNode | - | Custom error indicator, replaces default error icon |
| renderLoading | (props: { item: T; activeIndex: number }) => ReactNode | - | Custom loading indicator, replaces default wave loader |
| renderNavigation | (props: NavigationRenderProps) => ReactNode | - | Custom navigation, replaces default vertical arrows |
| renderNestedNavigation | (props: NavigationRenderProps) => ReactNode | - | Custom navigation for nested horizontal slider (multi-media posts), replaces default left/right arrows |
| renderNestedSlide | (props: NestedSlideRenderProps) => ReactNode | - | Custom slide renderer for nested horizontal slider items. Use props.defaultContent to wrap or embed the default ImageSlide/VideoSlide. Unlike renderSlide, null is not treated as a fallback. |
| renderSlide | (props: SlideRenderProps) => ReactNode | null | - | Custom slide rendering. Return null to fall back to default. Use props.defaultContent to wrap or embed the default slide. |
| renderSlideOverlay | (item, index, isActive) => ReactNode | - | Custom overlay per slide, replaces default SlideOverlay. Return null to hide. |
| renderTimeline | (props: TimelineRenderProps) => ReactNode | - | Custom playback timeline bar. Invoked only when gating rules would render the default bar (same auto/always/never + timelineMinDurationSeconds logic). Use props.defaultContent to wrap the built-in <TimelineBar />; return null to hide. |
Callbacks
| Prop | Type | Description |
|---|---|---|
| onClose | () => void | Called when player closes |
| onSlideChange | (index: number) => void | Called after slide change |
Reel Props (proxied)
These props are forwarded to the underlying Reel component.
| Prop | Type | Default | Description |
|---|---|---|---|
| enableNavKeys | boolean | true | Enable keyboard navigation |
| enableWheel | boolean | true | Enable mouse wheel navigation |
| loop | boolean | false | Enable infinite loop |
| swipeDistanceFactor | number | 0.12 | Swipe threshold (0-1) |
| transitionDuration | number | 300 | Transition animation duration (ms) |
| wheelDebounceMs | number | 200 | Wheel debounce duration (ms) |
Types
BaseContentItem
The generic constraint type. Extend this to use custom data types with ReelPlayerOverlay.
ContentItem
MediaItem
MediaType
ControlsRenderProps<T>
NavigationRenderProps
SlideRenderProps<T>
NestedSlideRenderProps
SlideOverlayProps
ImageSlideProps
VideoSlideProps
CloseButtonProps
SoundButtonProps
TimelineBarProps
TimelineRenderProps<T>
Sub-Components
Reusable building blocks exported for composition in custom render props:
CloseButton
Standalone close button with default reel-player styling. Use inside renderControls.
SoundButton
Standalone sound toggle. Must be inside a SoundProvider (automatically provided by ReelPlayerOverlay).
TimelineBar
Default playback scrub bar. Reads from the nearest TimelineProvider (automatically mounted inside ReelPlayerOverlay) and renders the track, buffered ranges, progress fill, and scrub pill. Theme via the --rk-reel-timeline-* custom properties, or replace via renderTimeline.
SlideOverlay
The default gradient overlay showing author, description, and likes. Rendered automatically when content has the required fields. Use renderSlideOverlay to replace or hide it.
ImageSlide
Image slide with lazy loading and object-fit: cover by default. Use inside renderSlide to compose custom image slides with your own styles.
VideoSlide
Video slide with shared <video> element for iOS sound continuity, poster frames, position memory, and loading indicator. Must be inside a SoundProvider (automatically provided by ReelPlayerOverlay).
Composing custom slides
renderSlide with ImageSlide / VideoSlide to customize media rendering while keeping all built-in behavior (autoplay, poster capture, sound sync).Content Loading & Error Handling
The player tracks per-slide loading and error states. A wave loader shows while content loads; an error icon shows for broken media. Errored URLs are cached so revisiting shows the error instantly without retrying.
Lifecycle Callbacks
When using renderSlide, call these callbacks to control the loading indicator:
| Callback | When to call |
|---|---|
| onReady | Image loaded or video started playing. Clears loading and error states. |
| onWaiting | Video is buffering mid-playback. Shows the loading indicator. |
| onError | Content failed to load. Shows error overlay and caches the URL as broken. |
Custom Loading & Error UI
Replace the default wave loader and error icon with custom components:
Timeline
The overlay renders a built-in playback timeline bar over the active video. The timeline prop gates rendering:
'auto'(default): renders when the active media is a video whose duration exceedstimelineMinDurationSeconds(default 30). Works for single-video slides and multi-media carousels; the bar follows the active nested item and hides on images.'always': renders whenever the active slide has a video.'never': never renders. Build a custom bar viarenderTimeline.
Theme via the --rk-reel-timeline-* CSS custom properties (height, colors, cursor size). For a fully custom scrub bar, timecode, or progress indicator, use renderTimeline; the callback receives a timelineState backed by the underlying TimelineController.
Sound Context
For custom implementations, you can access the sound state:
CSS Classes
All CSS classes are plain (not CSS modules), so they can be targeted with higher-specificity selectors in a stylesheet loaded after @reelkit/react-reel-player/styles.css. For color, size, and z-index changes, prefer the CSS custom properties documented in the Theming section below — they're designed for exactly that.
| Class | Component | Description |
|---|---|---|
| .rk-reel-overlay | Overlay | Fixed full-screen backdrop (background, z-index) |
| .rk-reel-container | Overlay | Player container (position, overflow) |
| .rk-reel-loader | Overlay | Wave loading animation overlay |
| .rk-reel-media-error | Overlay | Error state overlay (centered icon + text) |
| .rk-reel-media-error-text | Overlay | Error message text |
| .rk-reel-button | Controls | Shared circular icon button (close, sound, nav arrows) |
| .rk-reel-close-btn | Controls | Close button |
| .rk-reel-sound-btn | Controls | Sound toggle button |
| .rk-reel-nav-arrows | Navigation | Desktop-only arrow container (hidden below 768px) |
| .rk-reel-nav-button | Navigation | Individual prev/next nav arrow |
| .rk-reel-slide-wrapper | Slide | Wrapper around media + overlay |
| .rk-reel-slide-overlay | SlideOverlay | Gradient overlay container |
| .rk-reel-slide-overlay-author | SlideOverlay | Author row (avatar + name) |
| .rk-reel-slide-overlay-avatar | SlideOverlay | Author avatar image |
| .rk-reel-slide-overlay-name | SlideOverlay | Author name text |
| .rk-reel-slide-overlay-description | SlideOverlay | Description text |
| .rk-reel-slide-overlay-likes | SlideOverlay | Likes row (heart + count) |
| .rk-reel-video-container | VideoSlide | Video wrapper (background, overflow) |
| .rk-reel-video-element | VideoSlide | The <video> element |
| .rk-reel-video-poster | VideoSlide | Poster image (fades out on play) |
| .rk-reel-video-poster.rk-visible | VideoSlide | State modifier applied to the poster while the video is paused/loading |
| .rk-reel-nested-indicator | NestedSlider | Dot pagination under multi-media slides (position varies desktop vs. touch) |
| .rk-reel-nested-nav | NestedSlider | Horizontal carousel arrows (hidden below 768px) |
| .rk-reel-nested-nav-next | NestedSlider | Nested next arrow position |
| .rk-reel-nested-nav-prev | NestedSlider | Nested prev arrow position |
| .rk-reel-timeline | TimelineBar | Scrub-bar wrapper. Reuse on custom `renderTimeline` roots to inherit flush-bottom positioning, safe-area padding, and touch-device slide-overlay clearance. |
| .rk-reel-timeline-track | TimelineBar | Track (unplayed region) |
| .rk-reel-timeline-buffered | TimelineBar | Buffered segments layer |
| .rk-reel-timeline-fill | TimelineBar | Played-progress fill |
| .rk-reel-timeline-cursor | TimelineBar | Scrub-handle pill (floats above the track) |
Theming
Every color, size, z-index, and transition lives in a CSS custom property. Override one or many at :root (or any ancestor of the overlay) to retheme without touching component source.
| Token | Default | Controls |
|---|---|---|
| --rk-reel-overlay-bg | #000 | Full-screen backdrop color |
| --rk-reel-overlay-z | 1000 | Overlay z-index |
| --rk-reel-button-bg | rgba(0, 0, 0, 0.5) | Default circular button background |
| --rk-reel-button-bg-hover | rgba(255, 255, 255, 0.1) | Nav arrow background (and base hover state) |
| --rk-reel-button-bg-hover-strong | rgba(255, 255, 255, 0.2) | Nav arrow hover background |
| --rk-reel-button-fg | #fff | Button icon color |
| --rk-reel-button-size | 44px | Button width / height |
| --rk-reel-button-radius | 50% | Button border-radius |
| --rk-reel-ui-z | 10 | Close / sound / nav z-index |
| --rk-reel-edge-padding | 16px | Edge inset for close / sound / nav arrows |
| --rk-reel-nav-gap | 8px | Spacing between stacked nav arrows |
| --rk-reel-transition | 0.2s | Hover transition duration |
| --rk-reel-loader-color | rgba(255, 255, 255, 0.12) | Wave loader gradient color |
| --rk-reel-loader-duration | 1.8s | Wave loader animation duration |
| --rk-reel-error-fg | rgba(255, 255, 255, 0.4) | Error icon and text color |
| --rk-reel-error-text-size | 13px | Error message font size |
| --rk-reel-slide-overlay-bg | linear-gradient(transparent, rgba(0, 0, 0, 0.7)) | Caption scrim gradient |
| --rk-reel-slide-overlay-padding | 48px 16px 16px | Caption inner padding |
| --rk-reel-slide-overlay-name-color | #fff | Author name color |
| --rk-reel-slide-overlay-description-color | rgba(255, 255, 255, 0.9) | Description text color |
| --rk-reel-slide-overlay-likes-color | rgba(255, 255, 255, 0.8) | Likes row text color |
| --rk-reel-video-bg | #000 | Letterbox background behind <video> |
| --rk-reel-nested-button-bg | rgba(0, 0, 0, 0.5) | Nested arrow background |
| --rk-reel-nested-button-bg-hover | rgba(255, 255, 255, 0.2) | Nested arrow hover background |
| --rk-reel-nested-button-size | 36px | Nested arrow size |
| --rk-reel-nested-edge-padding | 12px | Nested arrow edge inset |
| --rk-reel-timeline-track | rgba(255, 255, 255, 0.22) | Track background (unplayed region) |
| --rk-reel-timeline-buffered | rgba(255, 255, 255, 0.4) | Buffered segments color |
| --rk-reel-timeline-fill | #fff | Played-progress fill color |
| --rk-reel-timeline-cursor | #fff | Scrub-handle pill color |
| --rk-reel-timeline-height | 3px | Track height at rest |
| --rk-reel-timeline-height-active | 6px | Track height on hover / focus / scrub |
| --rk-reel-timeline-cursor-width | 10px | Scrub-pill width at rest |
| --rk-reel-timeline-cursor-width-active | 14px | Scrub-pill width while scrubbing |
| --rk-reel-timeline-cursor-height | 24px | Scrub-pill height at rest |
| --rk-reel-timeline-cursor-height-active | 32px | Scrub-pill height while scrubbing |
| --rk-reel-timeline-hitbox | 16px | Extra pointer hit-area above the track |
| --rk-reel-timeline-transition | 0.15s ease-out | Track + pill grow/shrink animation |
| --rk-reel-timeline-z | 11 | Timeline z-index (above the default UI layer) |
Drop the snippet below into a stylesheet loaded after @reelkit/react-reel-player/styles.css.
Accessibility
The overlay root is a modal dialog (role="dialog", aria-modal="true"). Set ariaLabel to change the screen-reader announcement; it defaults to "Video player". Each slide carries role="group", aria-roledescription="slide", and an aria-label="Slide N of M", so swiping announces position in the sequence.
The overlay captures focus on open and returns it to the trigger on close. Tab and Shift+Tab cycle through focusable elements inside; focus that escapes (click outside, programmatic focus) gets pulled back. Implemented with captureFocusForReturn and createFocusTrap from @reelkit/core.
Keyboard Shortcuts
| Key | Action |
|---|---|
| ArrowUp | Previous slide |
| ArrowDown | Next slide |
| ArrowLeft | Previous media (in nested slider) |
| ArrowRight | Next media (in nested slider) |
| Escape | Close player |