media-time-slider
A slider component for seeking through media playback time
Import
import { TimeSlider } from '@videojs/react';import '@videojs/html/ui/time-slider';Anatomy
<TimeSlider.Root>
<TimeSlider.Track>
<TimeSlider.Buffer />
<TimeSlider.Fill />
</TimeSlider.Track>
<TimeSlider.Thumb />
<TimeSlider.Preview>
<TimeSlider.ChapterTitle />
<TimeSlider.Value type="pointer" />
</TimeSlider.Preview>
</TimeSlider.Root><media-time-slider>
<media-slider-track>
<media-slider-buffer></media-slider-buffer>
<media-slider-fill></media-slider-fill>
</media-slider-track>
<media-slider-thumb></media-slider-thumb>
<media-slider-preview>
<media-time-slider-chapter-title></media-time-slider-chapter-title>
<media-slider-value type="pointer"></media-slider-value>
</media-slider-preview>
</media-time-slider>Add Chapters to divide the slider track into sections. Each cue in the default kind="chapters" text track marks a chapter. Without chapter cues, one section covers the full slider.
<TimeSlider.Chapters
renderChapter={(props, state) => (
<div {...props}>
<TimeSlider.Track>
<TimeSlider.Buffer />
<TimeSlider.Fill />
</TimeSlider.Track>
</div>
)}
/>renderChapter receives the attributes, styles, and state for each normalized chapter range. When no chapter cues are available, it renders once as a full-width, cue-less range.
<media-time-slider-chapters>
<template>
<div>
<media-slider-track>
<media-slider-buffer></media-slider-buffer>
<media-slider-fill></media-slider-fill>
</media-slider-track>
</div>
</template>
</media-time-slider-chapters>The required template must contain exactly one HTML root element. It remains in the light DOM and is cloned for each normalized chapter range. When no chapter cues are available, it is cloned once as a full-width, cue-less range.
Behavior
Displays and controls the current playback position. Dragging the slider seeks the media. The fill level reflects currentTime / duration as a percentage, and the buffer level shows how much media has been buffered.
Value changes during drag are throttled via the changeThrottle prop (default 100ms) using a leading+trailing throttle to keep the UI responsive without overwhelming the media element.
Chapter cues are normalized into contiguous ranges. Any uncovered time becomes a gap range, so track geometry always spans the full duration. With no usable chapter cues, one gap range spans the entire slider. Gap ranges receive the same geometry styles but cannot be highlighted and have no chapter title.
Styling
Use CSS custom properties to style the fill, pointer, and buffer levels:
media-time-slider::before {
width: var(--media-slider-fill);
}React renders a <div> element. Add a className to style it:
.time-slider::before {
width: var(--media-slider-fill);
}Use data-seeking to style during active seek operations:
media-time-slider[data-seeking] {
opacity: 0.8;
}.time-slider[data-seeking] {
opacity: 0.8;
}Each rendered chapter receives data-active while playback is inside it and data-highlighted while the pointer is inside an authored chapter. Its geometry and local progress are exposed through:
--media-slider-chapter-start--media-slider-chapter-end--media-slider-chapter-width--media-slider-chapter-fill--media-slider-chapter-buffer
The generated fill and buffer parts also inherit the slider-level --media-slider-fill and --media-slider-buffer values. This allows every chapter to render full-width progress planes and use its chapter bounds as a clip, keeping progress transitions continuous across segments.
Accessibility
Renders with role="slider" and an automatic aria-label that resolves to “Seek” from the active locale. Override with the label prop. Keyboard controls:
- Arrow Left / Arrow Right: step by
stepincrement - Page Up / Page Down: step by
largeStepincrement - Home: seek to start
- End: seek to end
ChapterTitle follows the pointer during pointer interaction. While the slider has keyboard focus, it follows the current playback position and announces chapter changes.
Examples
Nest sub-components for full control over the slider’s DOM structure. This example includes a track, fill bar, buffer indicator, draggable thumb, and a tooltip that shows the pointed-at time.
import { Container, createPlayer, TimeSlider } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function WithParts() {
return (
<Player>
<Container className="media-container">
<Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoPlay muted playsInline loop />
<TimeSlider.Root className="media-time-slider">
<TimeSlider.Track className="media-slider-track">
<TimeSlider.Buffer className="media-slider-buffer" />
<TimeSlider.Fill className="media-slider-fill" />
</TimeSlider.Track>
<TimeSlider.Thumb className="media-slider-thumb" />
<TimeSlider.Value type="pointer" className="media-slider-value" />
</TimeSlider.Root>
</Container>
</Player>
);
}
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.media-time-slider {
position: absolute;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
height: 20px;
cursor: pointer;
}
.media-slider-track {
position: absolute;
right: 0;
left: 0;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 9999px;
transition: height 150ms ease;
}
.media-time-slider[data-interactive] .media-slider-track {
height: 6px;
}
.media-slider-buffer {
position: absolute;
top: 0;
left: 0;
width: var(--media-slider-buffer);
height: 100%;
background: rgba(255, 255, 255, 0.4);
border-radius: 9999px;
}
.media-slider-fill {
position: absolute;
top: 0;
left: 0;
width: var(--media-slider-fill);
height: 100%;
background: white;
border-radius: 9999px;
}
.media-slider-thumb {
position: absolute;
left: var(--media-slider-fill);
width: 14px;
height: 14px;
background: white;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
transform: translateX(-50%) scale(0);
transition: transform 150ms ease;
}
.media-time-slider[data-interactive] .media-slider-thumb {
transform: translateX(-50%) scale(1);
}
.media-time-slider[data-dragging] .media-slider-thumb {
left: var(--media-slider-pointer);
transform: translateX(-50%) scale(1.1);
}
.media-time-slider[data-dragging] .media-slider-fill {
width: var(--media-slider-pointer);
}
.media-slider-value {
position: absolute;
bottom: 100%;
left: var(--media-slider-pointer);
padding: 2px 6px;
margin-bottom: 6px;
font-size: 12px;
color: white;
white-space: nowrap;
pointer-events: none;
background: rgba(0, 0, 0, 0.8);
border-radius: 4px;
opacity: 0;
transform: translateX(-50%);
transition: opacity 150ms ease;
}
.media-time-slider[data-pointing] .media-slider-value {
opacity: 1;
}
<video-player class="video-player">
<media-container>
<video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoplay muted playsinline loop></video>
<media-time-slider class="media-time-slider">
<media-slider-track class="media-slider-track">
<media-slider-buffer class="media-slider-buffer"></media-slider-buffer>
<media-slider-fill class="media-slider-fill"></media-slider-fill>
</media-slider-track>
<media-slider-thumb class="media-slider-thumb"></media-slider-thumb>
<media-slider-value type="pointer" class="media-slider-value"></media-slider-value>
</media-time-slider>
</media-container>
</video-player>
.video-player media-container {
position: relative;
display: block;
}
.video-player media-container video {
width: 100%;
}
.media-time-slider {
position: absolute;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
height: 20px;
cursor: pointer;
}
.media-slider-track {
position: absolute;
right: 0;
left: 0;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 9999px;
transition: height 150ms ease;
}
.media-time-slider[data-interactive] .media-slider-track {
height: 6px;
}
.media-slider-buffer {
position: absolute;
top: 0;
left: 0;
width: var(--media-slider-buffer);
height: 100%;
background: rgba(255, 255, 255, 0.4);
border-radius: 9999px;
}
.media-slider-fill {
position: absolute;
top: 0;
left: 0;
width: var(--media-slider-fill);
height: 100%;
background: white;
border-radius: 9999px;
}
.media-slider-thumb {
position: absolute;
left: var(--media-slider-fill);
width: 14px;
height: 14px;
background: white;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
transform: translateX(-50%) scale(0);
transition: transform 150ms ease;
}
.media-time-slider[data-interactive] .media-slider-thumb {
transform: translateX(-50%) scale(1);
}
.media-time-slider[data-dragging] .media-slider-thumb {
left: var(--media-slider-pointer);
transform: translateX(-50%) scale(1.1);
}
.media-time-slider[data-dragging] .media-slider-fill {
width: var(--media-slider-pointer);
}
.media-slider-value {
position: absolute;
bottom: 100%;
left: var(--media-slider-pointer);
padding: 2px 6px;
margin-bottom: 6px;
font-size: 12px;
color: white;
white-space: nowrap;
pointer-events: none;
background: rgba(0, 0, 0, 0.8);
border-radius: 4px;
opacity: 0;
transform: translateX(-50%);
transition: opacity 150ms ease;
}
.media-time-slider[data-pointing] .media-slider-value {
opacity: 1;
}
import '@videojs/html/video/player';
import '@videojs/html/ui/container';
import '@videojs/html/ui/time-slider';
With chapters
Add a default chapters track, then provide the markup used for each chapter. The deliberate one-minute gap in this example remains visible as an unlabelled, non-highlightable segment.
import { Container, createPlayer, TimeSlider } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function WithChapters() {
return (
<Player>
<Container className="media-container">
<Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoPlay muted playsInline loop crossOrigin="anonymous">
<track kind="chapters" src="/docs/demos/time-slider/chapters.vtt" srcLang="en" default />
</Video>
<TimeSlider.Root className="media-time-slider">
<TimeSlider.Chapters
className="media-slider-chapters"
renderChapter={(props) => (
<div {...props} className="media-slider-chapter">
<TimeSlider.Track className="media-slider-track">
<TimeSlider.Buffer className="media-slider-buffer" />
<TimeSlider.Fill className="media-slider-fill" />
</TimeSlider.Track>
</div>
)}
/>
<TimeSlider.Thumb className="media-slider-thumb" />
<TimeSlider.Preview className="media-slider-preview">
<TimeSlider.ChapterTitle className="media-slider-chapter-title" />
<TimeSlider.Value type="pointer" />
</TimeSlider.Preview>
</TimeSlider.Root>
</Container>
</Player>
);
}
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.media-time-slider {
position: absolute;
right: 12px;
bottom: 8px;
left: 12px;
display: flex;
align-items: center;
height: 20px;
color: white;
cursor: pointer;
}
.media-slider-chapters {
position: relative;
width: 100%;
height: 100%;
}
.media-slider-chapter {
position: absolute;
inset: 0;
display: flex;
align-items: center;
clip-path: inset(0 calc(100% - var(--media-slider-chapter-end)) 0 var(--media-slider-chapter-start));
}
.media-slider-track {
position: absolute;
right: 0;
left: 0;
height: 4px;
overflow: hidden;
background: rgb(255 255 255 / 30%);
border-radius: 9999px;
transition: height 150ms ease;
}
.media-slider-chapter .media-slider-track {
clip-path: inset(
0 calc(100% - var(--media-slider-chapter-end) + 2px) 0 calc(var(--media-slider-chapter-start) + 2px) round 9999px
);
}
.media-slider-chapter[data-highlighted] .media-slider-track {
height: 7px;
}
.media-slider-buffer,
.media-slider-fill {
position: absolute;
inset-block: 0;
left: 0;
border-radius: inherit;
transition: width 200ms linear;
}
.media-slider-buffer {
width: var(--media-slider-buffer);
background: rgb(255 255 255 / 30%);
}
.media-slider-fill {
width: var(--media-slider-fill);
background: white;
}
.media-time-slider[data-dragging] .media-slider-fill {
width: var(--media-slider-pointer);
transition-duration: 0ms;
}
.media-slider-thumb {
position: absolute;
top: 50%;
left: var(--media-slider-fill);
width: 12px;
height: 12px;
background: white;
border-radius: 50%;
translate: -50% -50%;
}
.media-time-slider[data-dragging] .media-slider-thumb {
left: var(--media-slider-pointer);
}
.media-slider-preview {
position: absolute;
bottom: 100%;
left: var(--media-slider-pointer);
display: flex;
gap: 6px;
padding: 3px 7px;
color: white;
white-space: nowrap;
pointer-events: none;
background: rgb(0 0 0 / 80%);
border-radius: 4px;
opacity: 0;
translate: -50% -4px;
}
.media-time-slider[data-pointing] .media-slider-preview,
.media-time-slider[data-interactive]:not([data-pointing]) .media-slider-preview {
opacity: 1;
}
.media-slider-chapter-title:empty {
display: none;
}
<video-player class="video-player">
<media-container>
<video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoplay muted playsinline loop crossorigin="anonymous">
<track kind="chapters" src="/docs/demos/time-slider/chapters.vtt" srclang="en" default />
</video>
<media-time-slider class="media-time-slider">
<media-time-slider-chapters class="media-slider-chapters">
<template>
<div class="media-slider-chapter">
<media-slider-track class="media-slider-track">
<media-slider-buffer class="media-slider-buffer"></media-slider-buffer>
<media-slider-fill class="media-slider-fill"></media-slider-fill>
</media-slider-track>
</div>
</template>
</media-time-slider-chapters>
<media-slider-thumb class="media-slider-thumb"></media-slider-thumb>
<media-slider-preview class="media-slider-preview">
<media-time-slider-chapter-title class="media-slider-chapter-title"></media-time-slider-chapter-title>
<media-slider-value type="pointer"></media-slider-value>
</media-slider-preview>
</media-time-slider>
</media-container>
</video-player>
.video-player media-container {
position: relative;
display: block;
}
.video-player media-container video {
width: 100%;
}
.media-time-slider {
position: absolute;
right: 12px;
bottom: 8px;
left: 12px;
display: flex;
align-items: center;
height: 20px;
color: white;
cursor: pointer;
}
.media-slider-chapters {
position: relative;
display: block;
width: 100%;
height: 100%;
}
.media-slider-chapter {
position: absolute;
inset: 0;
display: flex;
align-items: center;
clip-path: inset(0 calc(100% - var(--media-slider-chapter-end)) 0 var(--media-slider-chapter-start));
}
.media-slider-track {
position: absolute;
right: 0;
left: 0;
display: block;
height: 4px;
overflow: hidden;
background: rgb(255 255 255 / 30%);
border-radius: 9999px;
transition: height 150ms ease;
}
.media-slider-chapter .media-slider-track {
clip-path: inset(
0 calc(100% - var(--media-slider-chapter-end) + 2px) 0 calc(var(--media-slider-chapter-start) + 2px) round 9999px
);
}
.media-slider-chapter[data-highlighted] .media-slider-track {
height: 7px;
}
.media-slider-buffer,
.media-slider-fill {
position: absolute;
inset-block: 0;
left: 0;
display: block;
border-radius: inherit;
transition: width 200ms linear;
}
.media-slider-buffer {
width: var(--media-slider-buffer);
background: rgb(255 255 255 / 30%);
}
.media-slider-fill {
width: var(--media-slider-fill);
background: white;
}
.media-time-slider[data-dragging] .media-slider-fill {
width: var(--media-slider-pointer);
transition-duration: 0ms;
}
.media-slider-thumb {
position: absolute;
top: 50%;
left: var(--media-slider-fill);
display: block;
width: 12px;
height: 12px;
background: white;
border-radius: 50%;
translate: -50% -50%;
}
.media-time-slider[data-dragging] .media-slider-thumb {
left: var(--media-slider-pointer);
}
.media-slider-preview {
position: absolute;
bottom: 100%;
left: var(--media-slider-pointer);
display: flex;
gap: 6px;
padding: 3px 7px;
color: white;
white-space: nowrap;
pointer-events: none;
background: rgb(0 0 0 / 80%);
border-radius: 4px;
opacity: 0;
translate: -50% -4px;
}
.media-time-slider[data-pointing] .media-slider-preview,
.media-time-slider[data-interactive]:not([data-pointing]) .media-slider-preview {
opacity: 1;
}
.media-slider-chapter-title:empty {
display: none;
}
import '@videojs/html/video/player';
import '@videojs/html/ui/container';
import '@videojs/html/ui/time-slider';
import '@videojs/html/ui/time-slider-chapters';
API Reference
Rootmedia-time-slider
Props
| Prop | Type | Default | Details |
|---|---|---|---|
changeThrottle | number | 100 | |
| |||
disabled | boolean | — | |
| |||
label | object | '' | |
| |||
largeStep | number | — | |
| |||
max | number | — | |
| |||
min | number | — | |
| |||
orientation | 'horizontal' | 'vertical' | — | |
| |||
pauseOnDrag | boolean | false | |
| |||
step | number | — | |
| |||
thumbAlignment | 'center' | 'edge' | — | |
| |||
value | number | — | |
| |||
State
render, className, and style props.| Property | Type | Details |
|---|---|---|
value | number | |
| ||
fillPercent | number | |
| ||
pointerPercent | number | |
| ||
dragging | boolean | |
| ||
pointing | boolean | |
| ||
interactive | boolean | |
| ||
orientation | 'horizontal' | 'vertical' | |
| ||
disabled | boolean | |
| ||
thumbAlignment | 'center' | 'edge' | |
| ||
currentTime | number | |
| ||
duration | number | |
| ||
seeking | boolean | |
| ||
bufferPercent | number | |
| ||
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-seeking | ||
| ||
CSS custom properties
| Variable | Details |
|---|---|
--media-slider-fill | |
| |
--media-slider-pointer | |
| |
--media-slider-buffer | |
| |
Events
| Event | Description |
|---|---|
drag-end | Fired when a pointer drag ends. |
drag-start | Fired when a pointer drag starts. |
ChapterTitlemedia-time-slider-chapter-title
Displays the chapter title at the current pointer or keyboard position.
Chaptersmedia-time-slider-chapters
Renders normalized chapter ranges across the full slider, including one full range when chapter cues are absent.
Buffermedia-slider-buffer
Displays the buffered range on the slider track.
Fillmedia-slider-fill
Displays the filled portion from start to the current value.
Previewmedia-slider-preview
Positioning container for preview content that tracks the pointer along the slider.
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-dragging | ||
| ||
data-pointing | ||
| ||
data-interactive | ||
| ||
data-orientation | 'horizontal' | 'vertical' | |
| ||
data-disabled | ||
| ||
Thumbmedia-slider-thumb
Draggable handle for setting the slider value. Receives focus and handles keyboard interaction.
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-dragging | ||
| ||
data-pointing | ||
| ||
data-interactive | ||
| ||
data-orientation | 'horizontal' | 'vertical' | |
| ||
data-disabled | ||
| ||
Trackmedia-slider-track
Contains the slider's visual track and interactive hit zone.
Valuemedia-slider-value
Displays a formatted text representation of the slider value. Renders an <output> element.
Props
| Prop | Type | Default | Details |
|---|---|---|---|
type | 'current' | 'pointer' | — | |
| |||
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-dragging | ||
| ||
data-pointing | ||
| ||
data-interactive | ||
| ||
data-orientation | 'horizontal' | 'vertical' | |
| ||
data-disabled | ||
| ||