Progress Bar Video Player
max is the video length in seconds, so value is the playback position and buffer-value is how far the video has loaded. The buffer stays 30 seconds ahead of the fill. The time label carries data-value-text, so assistive technology hears "1:05 of 3:20" instead of a raw number.
Code
The following example assumes that you have imported the component and set up the theme.
<div class="video-player" data-state="paused">24 collapsed lines
<div class="video-screen" aria-hidden="true"> <svg viewBox="0 0 24 24"> <path d="M8 5v14l11-7z" /> </svg> </div>
<div class="video-controls"> <button type="button" class="play-button" aria-label="Play"> <!-- Play --> <svg class="play-icon" aria-hidden="true" viewBox="0 0 24 24"> <path d="M8 5v14l11-7z" /> </svg>
<!-- Pause --> <svg class="pause-icon" aria-hidden="true" viewBox="0 0 24 24"> <path d="M6 5h4v14H6zm8 0h4v14h-4z" /> </svg> </button>
<sv-progress-bar aria-label="Playback position" buffer-value="0" max="200" value="0"> <span>trailer.mp4</span> <span class="video-time" data-value-text>0:00 of 3:20</span> </sv-progress-bar> </div></div>
<script type="application/x-typescript" module>48 collapsed lines
const player = document.querySelector<HTMLElement>(".video-player")!; const bar = player.querySelector<ProgressBar>("sv-progress-bar")!; const time = player.querySelector(".video-time")!; const playButton = player.querySelector(".play-button")!;
let intervalId: ReturnType<typeof setInterval> | undefined;
playButton.addEventListener("click", () => { if (player.dataset.state === "playing") { pause(); } else { play(); } });
function play() { if (bar.value === bar.max) { bar.value = 0; }
player.dataset.state = "playing"; playButton.ariaLabel = "Pause"; intervalId = setInterval(tick, 250); }
function pause() { player.dataset.state = "paused"; playButton.ariaLabel = "Play"; clearInterval(intervalId); }
function tick() { const position = (bar.value ?? 0) + 1;
time.textContent = `${formatTime(position)} of ${formatTime(bar.max)}`; bar.value = position; bar.bufferValue = position + 30;
if (position === bar.max) { pause(); } }
function formatTime(seconds: number) { const minutes = Math.floor(seconds / 60); const remainingSeconds = String(Math.floor(seconds % 60)).padStart(2, "0"); return `${minutes}:${remainingSeconds}`; }</script>
<style>83 collapsed lines
.video-player { display: grid; max-inline-size: 26rem; margin: auto; overflow: hidden; background-color: var(--sv-gray-7); border: 1px solid var(--sv-gray-5); border-radius: var(--sv-bdr-rad-lg); }
.video-screen { display: grid; place-items: center; aspect-ratio: 16 / 9; background-color: #000;
svg { inline-size: 3rem; opacity: 0.6; fill: #fff; transition: opacity 200ms ease; } }
.video-controls { display: grid; grid-template-columns: auto 1fr; gap: 0.75rem; align-items: center; padding: 0.75rem 1rem;
sv-progress-bar { --sv-progress-bar-gap: 0.35em;
font-size: 0.8rem; }
.video-time { font-family: monospace; color: var(--sv-gray-3); } }
.play-button { display: grid; place-items: center; inline-size: 2.25rem; block-size: 2.25rem; padding: 0; color: var(--sv-accent-content); cursor: pointer; background-color: var(--sv-accent); border: none; border-radius: 50%;
svg { inline-size: 1.25rem; fill: currentcolor; }
.pause-icon { display: none; }
&:focus-visible { outline: 2px solid var(--sv-accent); outline-offset: 2px; } }
.video-player[data-state="playing"] { .video-screen svg { opacity: 0; }
.play-icon { display: none; }
.pause-icon { display: block; } }</style>