Open demo

CSS code

HTML
<div><span class="lbl">Wide container</span>
<div class="m8" data-vp="mo" data-title="Fractal Tide" data-autohide role="group" aria-label="Fractal Tide video player" tabindex="-1">
  <video src="../media/clip-1.mp4" poster="../media/clip-1-poster.jpg" preload="metadata" playsinline controls>
    <track kind="captions" src="../media/clip-1-captions.vtt" srclang="en" label="English" default>
  </video>
  <p class="m8-title" aria-hidden="true">Fractal Tide</p>
  <p class="m8-cc" data-cc-box hidden></p>
  <div class="m8-bar">
    <input type="range" data-seek min="0" max="16" step="0.1" value="0" aria-label="Seek" aria-valuetext="0:00 of 0:16">
    <div class="m8-row">
      <button type="button" class="m8-btn" data-play aria-label="Play Fractal Tide"><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
      <button type="button" class="m8-btn m8-skip" data-skip="-10" aria-label="Back 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3.5 4v4.5H8"/><text x="12.6" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <button type="button" class="m8-btn m8-skip" data-skip="10" aria-label="Forward 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 12a8.5 8.5 0 1 1-2.6-6.1"/><path d="M20.5 4v4.5H16"/><text x="11.4" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <div class="m8-volgroup">
        <button type="button" class="m8-btn" data-mute aria-label="Mute" aria-pressed="false"><svg class="i-vol" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/></svg><svg class="i-muted" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="m16 9 6 6m0-6-6 6"/></svg></button>
        <input type="range" class="m8-vol" data-vol min="0" max="1" step="0.05" value="1" aria-label="Volume" aria-valuetext="100%">
      </div>
      <span class="m8-time"><span data-cur>0:00</span><span class="m8-dur"> / <span data-dur>0:16</span></span></span>
      <span class="m8-spacer"></span>
      <button type="button" class="m8-btn m8-speed" data-speed><span class="m8-sr">Playback speed </span><span data-speed-val>1×</span></button>
      <button type="button" class="m8-btn" data-cc aria-label="Captions" aria-pressed="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><path d="M10.5 10.2a2.3 2.3 0 1 0 0 3.6M16.5 10.2a2.3 2.3 0 1 0 0 3.6"/></svg></button>
      <button type="button" class="m8-btn" data-pip aria-label="Picture in picture" aria-pressed="false" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2.5"/><rect x="12" y="11.5" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg></button>
      <button type="button" class="m8-btn" data-fs aria-label="Full screen"><svg class="i-expand" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg><svg class="i-shrink" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg></button>
    </div>
  </div>
</div>
</div>

<div><span class="lbl">Narrow container, 320 pixels</span>
<div class="demo-narrow">
<div class="m8" data-vp="mo" data-title="Slow Gradients" data-autohide role="group" aria-label="Slow Gradients video player" tabindex="-1">
  <video src="../media/clip-4.mp4" poster="../media/clip-4-poster.jpg" preload="metadata" playsinline controls>
    <track kind="captions" src="../media/clip-4-captions.vtt" srclang="en" label="English" default>
  </video>
  <p class="m8-title" aria-hidden="true">Slow Gradients</p>
  <p class="m8-cc" data-cc-box hidden></p>
  <div class="m8-bar">
    <input type="range" data-seek min="0" max="12" step="0.1" value="0" aria-label="Seek" aria-valuetext="0:00 of 0:12">
    <div class="m8-row">
      <button type="button" class="m8-btn" data-play aria-label="Play Slow Gradients"><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
      <button type="button" class="m8-btn m8-skip" data-skip="-10" aria-label="Back 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3.5 4v4.5H8"/><text x="12.6" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <button type="button" class="m8-btn m8-skip" data-skip="10" aria-label="Forward 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 12a8.5 8.5 0 1 1-2.6-6.1"/><path d="M20.5 4v4.5H16"/><text x="11.4" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <div class="m8-volgroup">
        <button type="button" class="m8-btn" data-mute aria-label="Mute" aria-pressed="false"><svg class="i-vol" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/></svg><svg class="i-muted" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="m16 9 6 6m0-6-6 6"/></svg></button>
        <input type="range" class="m8-vol" data-vol min="0" max="1" step="0.05" value="1" aria-label="Volume" aria-valuetext="100%">
      </div>
      <span class="m8-time"><span data-cur>0:00</span><span class="m8-dur"> / <span data-dur>0:12</span></span></span>
      <span class="m8-spacer"></span>
      <button type="button" class="m8-btn m8-speed" data-speed><span class="m8-sr">Playback speed </span><span data-speed-val>1×</span></button>
      <button type="button" class="m8-btn" data-cc aria-label="Captions" aria-pressed="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><path d="M10.5 10.2a2.3 2.3 0 1 0 0 3.6M16.5 10.2a2.3 2.3 0 1 0 0 3.6"/></svg></button>
      <button type="button" class="m8-btn" data-pip aria-label="Picture in picture" aria-pressed="false" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2.5"/><rect x="12" y="11.5" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg></button>
      <button type="button" class="m8-btn" data-fs aria-label="Full screen"><svg class="i-expand" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg><svg class="i-shrink" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg></button>
    </div>
  </div>
</div>
</div></div>

<div><span class="lbl">Resizable container</span>
<div class="demo-resize">
<div class="m8" data-vp="mo" data-title="Life in Colour" data-autohide role="group" aria-label="Life in Colour video player" tabindex="-1">
  <video src="../media/clip-3.mp4" poster="../media/clip-3-poster.jpg" preload="metadata" playsinline controls>
    <track kind="captions" src="../media/clip-3-captions.vtt" srclang="en" label="English" default>
  </video>
  <p class="m8-title" aria-hidden="true">Life in Colour</p>
  <p class="m8-cc" data-cc-box hidden></p>
  <div class="m8-bar">
    <input type="range" data-seek min="0" max="14" step="0.1" value="0" aria-label="Seek" aria-valuetext="0:00 of 0:14">
    <div class="m8-row">
      <button type="button" class="m8-btn" data-play aria-label="Play Life in Colour"><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
      <button type="button" class="m8-btn m8-skip" data-skip="-10" aria-label="Back 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3.5 4v4.5H8"/><text x="12.6" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <button type="button" class="m8-btn m8-skip" data-skip="10" aria-label="Forward 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 12a8.5 8.5 0 1 1-2.6-6.1"/><path d="M20.5 4v4.5H16"/><text x="11.4" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <div class="m8-volgroup">
        <button type="button" class="m8-btn" data-mute aria-label="Mute" aria-pressed="false"><svg class="i-vol" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/></svg><svg class="i-muted" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="m16 9 6 6m0-6-6 6"/></svg></button>
        <input type="range" class="m8-vol" data-vol min="0" max="1" step="0.05" value="1" aria-label="Volume" aria-valuetext="100%">
      </div>
      <span class="m8-time"><span data-cur>0:00</span><span class="m8-dur"> / <span data-dur>0:14</span></span></span>
      <span class="m8-spacer"></span>
      <button type="button" class="m8-btn m8-speed" data-speed><span class="m8-sr">Playback speed </span><span data-speed-val>1×</span></button>
      <button type="button" class="m8-btn" data-cc aria-label="Captions" aria-pressed="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><path d="M10.5 10.2a2.3 2.3 0 1 0 0 3.6M16.5 10.2a2.3 2.3 0 1 0 0 3.6"/></svg></button>
      <button type="button" class="m8-btn" data-pip aria-label="Picture in picture" aria-pressed="false" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2.5"/><rect x="12" y="11.5" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg></button>
      <button type="button" class="m8-btn" data-fs aria-label="Full screen"><svg class="i-expand" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg><svg class="i-shrink" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg></button>
    </div>
  </div>
</div>
</div>
<p class="demo-note">Drag the bottom-right corner of the dashed box: the control bar changes with the box, not the window.</p>
</div>
CSS
/* Modern CSS video player. The control bar answers to the player's own width with container queries, not the
   viewport; :has() reacts to control state (muted, picture in picture, keyboard focus); and @starting-style fades
   the bar and captions in when they appear. The script sets --p, --b, --v and toggles [data-playing] and [data-idle]. */
.m8{--m8-accent:#a3e635;--m8-ink:#fafafa;--p:0;--b:0;--v:1;
  container:m8 / inline-size;position:relative;overflow:hidden;border-radius:22px;background:#18181b;color:var(--m8-ink);isolation:isolate;
  box-shadow:0 20px 44px rgba(24,24,27,.22)}
.m8:focus{outline:none}
.m8 [hidden]{display:none !important}
.m8 video{display:block;width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;background:#18181b}
.m8[data-fullscreen]{border-radius:0}
.m8[data-fullscreen] video{height:100%;object-fit:contain}
.m8 button{font:inherit;color:inherit;cursor:pointer}
.m8-btn{position:relative;flex:none;display:grid;place-items:center;width:38px;height:38px;padding:0;border:0;border-radius:12px;background:none}
.m8-btn:hover{background:rgba(255,255,255,.14)}
.m8-btn:focus-visible{outline:2px solid var(--m8-accent);outline-offset:1px}
.m8-btn svg{display:block;width:21px;height:21px}
.m8 .i-pause,.m8[data-playing] .i-play,.m8 .i-shrink,.m8[data-fullscreen] .i-expand,.m8 .i-muted,.m8 [aria-pressed="true"] > .i-vol{display:none}
.m8[data-playing] .i-pause,.m8[data-fullscreen] .i-shrink,.m8 [aria-pressed="true"] > .i-muted{display:block}
.m8 [aria-pressed="true"].m8-btn:not([data-mute]){color:#18181b;background:var(--m8-accent)}
.m8-speed{width:auto;min-width:46px;padding:0 9px;font-size:.82rem;font-weight:700;font-variant-numeric:tabular-nums}
.m8-time{padding:0 6px;font-size:.82rem;font-variant-numeric:tabular-nums;white-space:nowrap}
.m8-spacer{flex:1}
.m8-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.m8-title{position:absolute;inset:0 0 auto;z-index:2;display:none;padding:16px 20px 40px;background:linear-gradient(rgba(0,0,0,.6),transparent);
  font-size:clamp(.95rem,2.4cqi,1.3rem);font-weight:700;pointer-events:none}
/* the bar fades in through @starting-style when the player is ready; while idle it only fades, so it stays in the tab order */
.m8-bar{position:absolute;inset:auto 0 0;z-index:2;display:block;padding:32px 12px 8px;background:linear-gradient(to top,rgba(9,9,11,.9),rgba(9,9,11,.5) 60%,transparent);
  transition:opacity .3s,translate .3s,display .3s allow-discrete}
@starting-style{.m8-bar{opacity:0;translate:0 10px}}
.m8[data-idle] .m8-bar{opacity:0;translate:0 10px;pointer-events:none}
.m8:not([data-ready]) .m8-bar,.m8:not([data-ready]) .m8-title{display:none}
/* :has() keeps the bar on screen while a control inside it has keyboard focus */
.m8:has(.m8-bar :focus-visible) .m8-bar{opacity:1;translate:0 0;pointer-events:auto}
.m8[data-idle]:not(:has(.m8-bar :focus-visible)){cursor:none}
.m8-row{display:flex;align-items:center;gap:2px;margin-top:6px}
/* volume slider grows out of the mute button on hover or focus, and dims while muted */
.m8-volgroup{display:flex;align-items:center}
.m8 .m8-volgroup .m8-vol{width:0;opacity:0;transition:width .25s,opacity .25s}
.m8 .m8-volgroup:has(:hover,:focus-visible) .m8-vol{width:84px;opacity:1}
.m8:has([data-mute][aria-pressed="true"]) .m8-vol{filter:grayscale(1) brightness(.8)}
.m8 input[type="range"]{-webkit-appearance:none;appearance:none;display:block;width:100%;min-width:0;height:16px;margin:0;background:none;cursor:pointer;
  --f:var(--p);--fb:var(--b);
  --fill:linear-gradient(to right,var(--m8-accent) 0 calc(7px + (100% - 14px) * var(--f)),rgba(255,255,255,.6) 0 calc(7px + (100% - 14px) * var(--fb)),rgba(255,255,255,.35) 0)}
.m8 input[type="range"].m8-vol{--f:var(--v);--fb:0;flex:none}
.m8 input[type="range"]:focus-visible{outline:none}
.m8 input[type="range"]::-webkit-slider-runnable-track{height:4px;border-radius:99px;background:var(--fill)}
.m8 input[type="range"]::-moz-range-track{height:4px;border-radius:99px;background:var(--fill)}
.m8 input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;margin-top:-5px;border:0;border-radius:50%;background:#fff}
.m8 input[type="range"]::-moz-range-thumb{width:14px;height:14px;border:0;border-radius:50%;background:#fff}
.m8 input[type="range"]:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #18181b,0 0 0 5px var(--m8-accent)}
.m8 input[type="range"]:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #18181b,0 0 0 5px var(--m8-accent)}
.m8-cc{position:absolute;left:50%;bottom:96px;z-index:1;width:max-content;max-width:88%;margin:0;padding:.3em .75em;border-radius:8px;translate:-50% 0;
  background:rgba(9,9,11,.85);color:#fff;font-size:clamp(.8rem,2.6cqi,1.1rem);line-height:1.4;text-align:center;white-space:pre-line;pointer-events:none;
  transition:opacity .2s,bottom .3s}
@starting-style{.m8-cc{opacity:0}}
.m8[data-idle] .m8-cc{bottom:16px}
/* picture in picture: :has() swaps the empty frame for a note */
.m8:has([data-pip][aria-pressed="true"])::after{content:"Playing in picture in picture";position:absolute;inset:0;z-index:1;display:grid;place-items:center;
  background:#18181b;color:#d4d4d8;font-size:.9rem}
/* container queries: the same markup sheds controls as the player gets narrower */
@container m8 (width >= 600px){.m8-title{display:block}.m8-btn{width:42px;height:42px}}
@container m8 (width < 520px){.m8-skip,.m8-volgroup .m8-vol{display:none}.m8-cc{bottom:88px}}
@container m8 (width < 400px){.m8-speed,.m8-dur{display:none}.m8-bar{padding:24px 8px 6px}.m8-btn{width:34px;height:34px}}
@container m8 (width < 300px){[data-pip],.m8-time{display:none}}
@media (prefers-reduced-motion:reduce){.m8-bar,.m8-cc,.m8-volgroup .m8-vol{transition:none}}
JavaScript
(() => {
// Video player core. Each [data-vp] wraps one native <video>; its controls are found by data attributes
// (data-play, data-seek, data-skip, data-mute, data-vol, data-cc, data-speed, data-fs, data-pip, data-cur, data-dur),
// so a player only needs the controls it shows. Without JavaScript the video keeps its native controls.
// Shortcuts work while focus is inside the player: Space or K play/pause, Left/Right seek 5 s, M mute,
// F full screen, C captions.
const vpFmt = s => { s = Math.max(0, Math.floor(s || 0)); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
function vpPlayer(root) {
  const $ = s => root.querySelector(s);
  const video = $('video'), plays = [...root.querySelectorAll('[data-play]')];
  const seek = $('[data-seek]'), vol = $('[data-vol]'), mute = $('[data-mute]'), cc = $('[data-cc]'), ccBox = $('[data-cc-box]');
  const stage = $('[data-stage]') || root; // the element that goes full screen
  const speed = $('[data-speed]'), fs = $('[data-fs]'), pip = $('[data-pip]'), cur = $('[data-cur]'), dur = $('[data-dur]');
  const RATES = [1, 1.25, 1.5, 2, 0.5, 0.75], listeners = [];
  let raf = 0, idle = 0, track = null, pointer = 'mouse';
  video.controls = false;
  root.setAttribute('data-ready', '');

  const length = () => (isFinite(video.duration) && video.duration) || (seek ? +seek.max : 0);
  const sync = () => {
    const t = video.currentTime, d = length();
    root.style.setProperty('--p', d ? Math.min(t / d, 1) : 0);
    if (cur) cur.textContent = vpFmt(t);
    if (seek) {
      seek.value = t;
      const text = vpFmt(t) + ' of ' + vpFmt(d);
      if (seek.getAttribute('aria-valuetext') !== text) seek.setAttribute('aria-valuetext', text);
    }
    listeners.forEach(fn => fn(t, d));
  };
  const buffered = () => {
    const b = video.buffered, d = length();
    let end = 0;
    for (let i = 0; i < b.length; i++) if (b.start(i) <= video.currentTime + 0.5) end = Math.max(end, b.end(i));
    root.style.setProperty('--b', d ? Math.min(end / d, 1) : 0);
  };
  const tick = () => { sync(); raf = requestAnimationFrame(tick); };
  const meta = () => { if (seek) seek.max = length(); if (dur) dur.textContent = vpFmt(length()); sync(); buffered(); };
  const jump = t => { video.currentTime = Math.min(Math.max(t, 0), length()); sync(); wake(); };
  const start = () => {
    // one player at a time; muted background loops (data-bg) keep running
    document.querySelectorAll('video, audio').forEach(m => { if (m !== video && !m.hasAttribute('data-bg')) m.pause(); });
    return video.play().catch(() => {});
  };
  const toggle = () => (video.paused || video.ended ? start() : video.pause());
  const name = () => root.dataset.title || 'video';
  const state = () => {
    const playing = !video.paused;
    root.toggleAttribute('data-playing', playing);
    plays.forEach(b => b.setAttribute('aria-label', (playing ? 'Pause ' : 'Play ') + name()));
    // if the focused play button was just hidden (a big centre button), hand focus to a visible one
    const a = document.activeElement;
    if (plays.includes(a) && !a.checkVisibility?.()) plays.find(b => b.checkVisibility?.())?.focus();
  };

  // Captions: the track stays in "hidden" mode and the script prints the active cue into [data-cc-box],
  // so the text sits above the control bar. Players without a box fall back to native rendering.
  // A player without a captions button shows captions when the root has data-captions.
  const ccOn = () => (cc ? cc.getAttribute('aria-pressed') === 'true' : root.hasAttribute('data-captions'));
  const render = () => {
    if (!ccBox) return;
    const cues = ccOn() && track && track.activeCues ? [...track.activeCues] : [];
    const text = cues.map(c => c.text.replace(/<[^>]+>/g, '')).join('\n');
    ccBox.textContent = text;
    ccBox.hidden = !text;
  };
  const captions = () => {
    track = [...video.textTracks].find(t => t.kind === 'captions' || t.kind === 'subtitles') || null;
    if (cc) cc.hidden = !track;
    if (!track) return render();
    track.mode = ccBox || !ccOn() ? 'hidden' : 'showing';
    track.oncuechange = render;
    render();
  };

  // Controls fade out after 2.5 s of playback without pointer movement, unless keyboard focus is inside.
  const keyboardFocus = () => { const a = document.activeElement; return !!a && a !== root && root.contains(a) && a.matches(':focus-visible'); };
  function wake() {
    root.removeAttribute('data-idle');
    clearTimeout(idle);
    if (!root.hasAttribute('data-autohide') || video.paused) return;
    idle = setTimeout(() => { if (!video.paused && !keyboardFocus()) root.setAttribute('data-idle', ''); }, 2500);
  }

  const fsElement = () => document.fullscreenElement || document.webkitFullscreenElement;
  const fullscreen = () => {
    if (fsElement() === stage) (document.exitFullscreen || document.webkitExitFullscreen).call(document);
    else if (stage.requestFullscreen) stage.requestFullscreen().catch(() => {});
    else if (stage.webkitRequestFullscreen) stage.webkitRequestFullscreen();
    else if (video.webkitEnterFullscreen) video.webkitEnterFullscreen(); // iPhone: native player
  };
  const fsSync = () => {
    const on = fsElement() === stage;
    root.toggleAttribute('data-fullscreen', on);
    if (fs) fs.setAttribute('aria-label', on ? 'Exit full screen' : 'Full screen');
  };

  plays.forEach(b => b.addEventListener('click', toggle));
  root.querySelectorAll('[data-skip]').forEach(b => b.addEventListener('click', () => jump(video.currentTime + +b.dataset.skip)));
  root.addEventListener('pointerdown', e => { pointer = e.pointerType; }, true);
  video.addEventListener('click', () => {
    // a tap on hidden controls only reveals them; a click toggles playback and gives the player focus for shortcuts
    if (pointer !== 'mouse' && root.hasAttribute('data-idle')) return wake();
    toggle();
    if (!root.contains(document.activeElement)) root.focus({ preventScroll: true });
  });
  video.addEventListener('play', () => { state(); tick(); wake(); });
  video.addEventListener('pause', () => { state(); cancelAnimationFrame(raf); sync(); wake(); });
  video.addEventListener('loadedmetadata', () => { meta(); captions(); });
  video.addEventListener('progress', buffered);
  video.addEventListener('seeked', () => { sync(); buffered(); });
  video.addEventListener('volumechange', () => {
    const v = video.muted ? 0 : video.volume;
    root.style.setProperty('--v', v);
    if (vol) { vol.value = v; vol.setAttribute('aria-valuetext', Math.round(v * 100) + '%'); }
    if (mute) mute.setAttribute('aria-pressed', String(v === 0));
  });
  video.addEventListener('ratechange', () => {
    if (speed) (speed.querySelector('[data-speed-val]') || speed).textContent = video.playbackRate + '×';
  });
  video.textTracks.addEventListener('addtrack', captions);
  video.querySelectorAll('track').forEach(t => t.addEventListener('load', captions));
  if (seek) seek.addEventListener('input', () => jump(+seek.value));
  if (vol) vol.addEventListener('input', () => { video.volume = +vol.value; video.muted = +vol.value === 0; });
  if (mute) mute.addEventListener('click', () => {
    if (video.muted || video.volume === 0) { video.muted = false; if (video.volume === 0) video.volume = 0.6; }
    else video.muted = true;
  });
  if (cc) cc.addEventListener('click', () => {
    cc.setAttribute('aria-pressed', String(!ccOn()));
    if (track && !ccBox) track.mode = ccOn() ? 'showing' : 'hidden';
    render();
  });
  if (speed) speed.addEventListener('click', () => {
    const rate = RATES[(RATES.indexOf(video.playbackRate) + 1) % RATES.length];
    video.playbackRate = video.defaultPlaybackRate = rate;
  });
  if (fs) {
    fs.hidden = !(stage.requestFullscreen || stage.webkitRequestFullscreen || video.webkitEnterFullscreen);
    fs.addEventListener('click', fullscreen);
    document.addEventListener('fullscreenchange', fsSync);
    document.addEventListener('webkitfullscreenchange', fsSync);
  }
  if (pip) {
    pip.hidden = !(document.pictureInPictureEnabled && !video.disablePictureInPicture);
    pip.addEventListener('click', () => (document.pictureInPictureElement === video
      ? document.exitPictureInPicture() : video.requestPictureInPicture()).catch(() => {}));
    video.addEventListener('enterpictureinpicture', () => pip.setAttribute('aria-pressed', 'true'));
    video.addEventListener('leavepictureinpicture', () => pip.setAttribute('aria-pressed', 'false'));
  }
  ['pointermove', 'focusin', 'focusout'].forEach(t => root.addEventListener(t, wake));
  root.addEventListener('pointerleave', () => {
    if (!root.hasAttribute('data-autohide') || video.paused || keyboardFocus()) return;
    clearTimeout(idle);
    idle = setTimeout(() => root.setAttribute('data-idle', ''), 600);
  });
  root.addEventListener('keydown', e => {
    if (e.altKey || e.ctrlKey || e.metaKey || e.defaultPrevented) return;
    const t = e.target, k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
    const control = t.matches('button, a[href], select, textarea, input:not([type="range"])');
    wake();
    if (k === 'k' || (k === ' ' && !control)) toggle();
    else if (k === 'm') video.muted = !video.muted;
    else if (k === 'f' && fs && !fs.hidden) fullscreen();
    else if (k === 'c' && cc && !cc.hidden) cc.click();
    else if ((k === 'ArrowLeft' || k === 'ArrowRight') && (t === seek || !t.matches('input[type="range"]')))
      jump(video.currentTime + (k === 'ArrowLeft' ? -5 : 5));
    else return;
    e.preventDefault();
  });

  // load() swaps the source, poster and captions file (playlists and lightboxes use it)
  const load = ({ src, poster, captions: vtt, title, duration }) => {
    video.pause();
    video.querySelectorAll('track').forEach(t => t.remove());
    if (title) root.dataset.title = title;
    video.poster = poster || '';
    video.src = src;
    if (vtt) {
      const t = Object.assign(document.createElement('track'), { kind: 'captions', srclang: 'en', label: 'English', src: vtt });
      t.addEventListener('load', captions);
      video.append(t);
    }
    if (seek && duration) seek.max = duration;
    if (dur && duration) dur.textContent = vpFmt(duration);
    video.load();
    state(); sync(); captions();
  };

  if (video.readyState > 0) meta();
  captions(); state(); fsSync();
  video.dispatchEvent(new Event('volumechange'));
  return { root, video, jump, start, toggle, load, onTime: fn => listeners.push(fn) };
}

document.querySelectorAll('[data-vp="mo"]').forEach(vpPlayer);
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Wide container</span>
<div class="m8" data-vp="mo" data-title="Fractal Tide" data-autohide role="group" aria-label="Fractal Tide video player" tabindex="-1">
  <div class="ratio ratio-16x9">
    <video src="../media/clip-1.mp4" poster="../media/clip-1-poster.jpg" preload="metadata" playsinline controls>
      <track kind="captions" src="../media/clip-1-captions.vtt" srclang="en" label="English" default>
    </video>
  </div>
  <p class="m8-title mb-0" aria-hidden="true">Fractal Tide</p>
  <p class="m8-cc" data-cc-box hidden></p>
  <div class="m8-bar">
    <input type="range" class="form-range" data-seek min="0" max="16" step="0.1" value="0" aria-label="Seek" aria-valuetext="0:00 of 0:16">
    <div class="m8-row d-flex align-items-center gap-1">
      <button type="button" class="btn m8-btn" data-play aria-label="Play Fractal Tide"><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
      <button type="button" class="btn m8-btn m8-skip" data-skip="-10" aria-label="Back 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3.5 4v4.5H8"/><text x="12.6" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <button type="button" class="btn m8-btn m8-skip" data-skip="10" aria-label="Forward 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 12a8.5 8.5 0 1 1-2.6-6.1"/><path d="M20.5 4v4.5H16"/><text x="11.4" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <div class="m8-volgroup d-flex align-items-center">
        <button type="button" class="btn m8-btn" data-mute aria-label="Mute" aria-pressed="false"><svg class="i-vol" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/></svg><svg class="i-muted" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="m16 9 6 6m0-6-6 6"/></svg></button>
        <input type="range" class="form-range m8-vol" data-vol min="0" max="1" step="0.05" value="1" aria-label="Volume" aria-valuetext="100%">
      </div>
      <span class="m8-time px-2 me-auto text-nowrap"><span data-cur>0:00</span><span class="m8-dur"> / <span data-dur>0:16</span></span></span>
      <button type="button" class="btn m8-btn m8-speed" data-speed><span class="visually-hidden">Playback speed </span><span data-speed-val>1×</span></button>
      <button type="button" class="btn m8-btn" data-cc aria-label="Captions" aria-pressed="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><path d="M10.5 10.2a2.3 2.3 0 1 0 0 3.6M16.5 10.2a2.3 2.3 0 1 0 0 3.6"/></svg></button>
      <button type="button" class="btn m8-btn" data-pip aria-label="Picture in picture" aria-pressed="false" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2.5"/><rect x="12" y="11.5" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg></button>
      <button type="button" class="btn m8-btn" data-fs aria-label="Full screen"><svg class="i-expand" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg><svg class="i-shrink" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg></button>
    </div>
  </div>
</div>
</div>

<div><span class="lbl">Narrow container, 320 pixels</span>
<div class="demo-narrow">
<div class="m8" data-vp="mo" data-title="Slow Gradients" data-autohide role="group" aria-label="Slow Gradients video player" tabindex="-1">
  <div class="ratio ratio-16x9">
    <video src="../media/clip-4.mp4" poster="../media/clip-4-poster.jpg" preload="metadata" playsinline controls>
      <track kind="captions" src="../media/clip-4-captions.vtt" srclang="en" label="English" default>
    </video>
  </div>
  <p class="m8-title mb-0" aria-hidden="true">Slow Gradients</p>
  <p class="m8-cc" data-cc-box hidden></p>
  <div class="m8-bar">
    <input type="range" class="form-range" data-seek min="0" max="12" step="0.1" value="0" aria-label="Seek" aria-valuetext="0:00 of 0:12">
    <div class="m8-row d-flex align-items-center gap-1">
      <button type="button" class="btn m8-btn" data-play aria-label="Play Slow Gradients"><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
      <button type="button" class="btn m8-btn m8-skip" data-skip="-10" aria-label="Back 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3.5 4v4.5H8"/><text x="12.6" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <button type="button" class="btn m8-btn m8-skip" data-skip="10" aria-label="Forward 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 12a8.5 8.5 0 1 1-2.6-6.1"/><path d="M20.5 4v4.5H16"/><text x="11.4" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <div class="m8-volgroup d-flex align-items-center">
        <button type="button" class="btn m8-btn" data-mute aria-label="Mute" aria-pressed="false"><svg class="i-vol" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/></svg><svg class="i-muted" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="m16 9 6 6m0-6-6 6"/></svg></button>
        <input type="range" class="form-range m8-vol" data-vol min="0" max="1" step="0.05" value="1" aria-label="Volume" aria-valuetext="100%">
      </div>
      <span class="m8-time px-2 me-auto text-nowrap"><span data-cur>0:00</span><span class="m8-dur"> / <span data-dur>0:12</span></span></span>
      <button type="button" class="btn m8-btn m8-speed" data-speed><span class="visually-hidden">Playback speed </span><span data-speed-val>1×</span></button>
      <button type="button" class="btn m8-btn" data-cc aria-label="Captions" aria-pressed="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><path d="M10.5 10.2a2.3 2.3 0 1 0 0 3.6M16.5 10.2a2.3 2.3 0 1 0 0 3.6"/></svg></button>
      <button type="button" class="btn m8-btn" data-pip aria-label="Picture in picture" aria-pressed="false" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2.5"/><rect x="12" y="11.5" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg></button>
      <button type="button" class="btn m8-btn" data-fs aria-label="Full screen"><svg class="i-expand" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg><svg class="i-shrink" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg></button>
    </div>
  </div>
</div>
</div></div>

<div><span class="lbl">Resizable container</span>
<div class="demo-resize">
<div class="m8" data-vp="mo" data-title="Life in Colour" data-autohide role="group" aria-label="Life in Colour video player" tabindex="-1">
  <div class="ratio ratio-16x9">
    <video src="../media/clip-3.mp4" poster="../media/clip-3-poster.jpg" preload="metadata" playsinline controls>
      <track kind="captions" src="../media/clip-3-captions.vtt" srclang="en" label="English" default>
    </video>
  </div>
  <p class="m8-title mb-0" aria-hidden="true">Life in Colour</p>
  <p class="m8-cc" data-cc-box hidden></p>
  <div class="m8-bar">
    <input type="range" class="form-range" data-seek min="0" max="14" step="0.1" value="0" aria-label="Seek" aria-valuetext="0:00 of 0:14">
    <div class="m8-row d-flex align-items-center gap-1">
      <button type="button" class="btn m8-btn" data-play aria-label="Play Life in Colour"><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
      <button type="button" class="btn m8-btn m8-skip" data-skip="-10" aria-label="Back 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3.5 4v4.5H8"/><text x="12.6" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <button type="button" class="btn m8-btn m8-skip" data-skip="10" aria-label="Forward 10 seconds"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 12a8.5 8.5 0 1 1-2.6-6.1"/><path d="M20.5 4v4.5H16"/><text x="11.4" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <div class="m8-volgroup d-flex align-items-center">
        <button type="button" class="btn m8-btn" data-mute aria-label="Mute" aria-pressed="false"><svg class="i-vol" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/></svg><svg class="i-muted" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="m16 9 6 6m0-6-6 6"/></svg></button>
        <input type="range" class="form-range m8-vol" data-vol min="0" max="1" step="0.05" value="1" aria-label="Volume" aria-valuetext="100%">
      </div>
      <span class="m8-time px-2 me-auto text-nowrap"><span data-cur>0:00</span><span class="m8-dur"> / <span data-dur>0:14</span></span></span>
      <button type="button" class="btn m8-btn m8-speed" data-speed><span class="visually-hidden">Playback speed </span><span data-speed-val>1×</span></button>
      <button type="button" class="btn m8-btn" data-cc aria-label="Captions" aria-pressed="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><path d="M10.5 10.2a2.3 2.3 0 1 0 0 3.6M16.5 10.2a2.3 2.3 0 1 0 0 3.6"/></svg></button>
      <button type="button" class="btn m8-btn" data-pip aria-label="Picture in picture" aria-pressed="false" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2.5"/><rect x="12" y="11.5" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg></button>
      <button type="button" class="btn m8-btn" data-fs aria-label="Full screen"><svg class="i-expand" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg><svg class="i-shrink" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg></button>
    </div>
  </div>
</div>
</div>
<p class="demo-note mb-0">Drag the bottom-right corner of the dashed box: the control bar changes with the box, not the window.</p>
</div>
CSS
/* Modern CSS video player on Bootstrap 5 (.btn controls, .form-range sliders, .visually-hidden). The control bar answers to the player's own width with container queries, not the
   viewport; :has() reacts to control state (muted, picture in picture, keyboard focus); and @starting-style fades
   the bar and captions in when they appear. The script sets --p, --b, --v and toggles [data-playing] and [data-idle]. */
.m8{--m8-accent:#a3e635;--m8-ink:#fafafa;--p:0;--b:0;--v:1;
  container:m8 / inline-size;position:relative;overflow:hidden;border-radius:22px;background:#18181b;color:var(--m8-ink);isolation:isolate;
  box-shadow:0 20px 44px rgba(24,24,27,.22)}
.m8:focus{outline:none}
.m8 [hidden]{display:none !important}
.m8 video{display:block;width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;background:#18181b}
.m8[data-fullscreen]{border-radius:0}
.m8[data-fullscreen] video{height:100%;object-fit:contain}
.m8[data-fullscreen] .ratio{height:100%}
.m8[data-fullscreen] .ratio::before{display:none}
.m8 button{font:inherit;color:inherit;cursor:pointer}
.m8 .m8-btn{--bs-btn-color:#fafafa;--bs-btn-hover-color:#fff;--bs-btn-active-color:#fff;--bs-btn-hover-bg:rgba(255,255,255,.14);--bs-btn-active-bg:rgba(255,255,255,.22);
  --bs-btn-border-width:0;--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-border-radius:12px;--bs-btn-focus-box-shadow:none;--bs-btn-font-size:.82rem;--bs-btn-font-weight:700;
  position:relative;flex:none;display:grid;place-items:center;width:38px;height:38px}
.m8 .m8-btn:focus-visible{outline:2px solid var(--m8-accent);outline-offset:1px}
.m8 .m8-btn svg{display:block;width:21px;height:21px}
.m8 .btn .i-pause,.m8[data-playing] .btn .i-play,.m8 .btn .i-shrink,.m8[data-fullscreen] .btn .i-expand,.m8 .btn .i-muted,.m8 .btn[aria-pressed="true"] > .i-vol{display:none}
.m8[data-playing] .btn .i-pause,.m8[data-fullscreen] .btn .i-shrink,.m8 .btn[aria-pressed="true"] > .i-muted{display:block}
.m8 .m8-btn[aria-pressed="true"]:not([data-mute]){--bs-btn-color:#18181b;--bs-btn-hover-color:#18181b;--bs-btn-bg:var(--m8-accent);--bs-btn-hover-bg:#bef264}
.m8 .m8-speed{width:auto;min-width:46px;padding:0 9px;font-variant-numeric:tabular-nums}
.m8-time{padding:0 6px;font-size:.82rem;font-variant-numeric:tabular-nums;white-space:nowrap}
.m8-spacer{flex:1}
.m8-title{position:absolute;inset:0 0 auto;z-index:2;display:none;padding:16px 20px 40px;background:linear-gradient(rgba(0,0,0,.6),transparent);
  font-size:clamp(.95rem,2.4cqi,1.3rem);font-weight:700;pointer-events:none}
/* the bar fades in through @starting-style when the player is ready; while idle it only fades, so it stays in the tab order */
.m8-bar{position:absolute;inset:auto 0 0;z-index:2;display:block;padding:32px 12px 8px;background:linear-gradient(to top,rgba(9,9,11,.9),rgba(9,9,11,.5) 60%,transparent);
  transition:opacity .3s,translate .3s,display .3s allow-discrete}
@starting-style{.m8-bar{opacity:0;translate:0 10px}}
.m8[data-idle] .m8-bar{opacity:0;translate:0 10px;pointer-events:none}
.m8:not([data-ready]) .m8-bar,.m8:not([data-ready]) .m8-title{display:none}
/* :has() keeps the bar on screen while a control inside it has keyboard focus */
.m8:has(.m8-bar :focus-visible) .m8-bar{opacity:1;translate:0 0;pointer-events:auto}
.m8[data-idle]:not(:has(.m8-bar :focus-visible)){cursor:none}
.m8-row{display:flex;align-items:center;gap:2px;margin-top:6px}
/* volume slider grows out of the mute button on hover or focus, and dims while muted */
.m8-volgroup{display:flex;align-items:center}
.m8 .m8-volgroup .m8-vol{width:0;opacity:0;transition:width .25s,opacity .25s}
.m8 .m8-volgroup:has(:hover,:focus-visible) .m8-vol{width:84px;opacity:1}
.m8:has([data-mute][aria-pressed="true"]) .m8-vol{filter:grayscale(1) brightness(.8)}
.m8 .form-range{display:block;min-width:0;height:16px;
  --f:var(--p);--fb:var(--b);
  --fill:linear-gradient(to right,var(--m8-accent) 0 calc(7px + (100% - 14px) * var(--f)),rgba(255,255,255,.6) 0 calc(7px + (100% - 14px) * var(--fb)),rgba(255,255,255,.35) 0)}
.m8 .form-range.m8-vol{--f:var(--v);--fb:0;flex:none}
@supports selector(:focus-visible){.m8 .form-range:focus::-webkit-slider-thumb{box-shadow:none}}
.m8 .form-range::-webkit-slider-runnable-track{height:4px;border-radius:99px;background:var(--fill)}
.m8 .form-range::-moz-range-track{height:4px;border-radius:99px;background:var(--fill)}
.m8 .form-range::-webkit-slider-thumb{width:14px;height:14px;margin-top:-5px;background:#fff;transition:none}
.m8 .form-range::-moz-range-thumb{width:14px;height:14px;border:0;background:#fff;transition:none}
.m8 .form-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #18181b,0 0 0 5px var(--m8-accent)}
.m8 .form-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #18181b,0 0 0 5px var(--m8-accent)}
.m8-cc{position:absolute;left:50%;bottom:96px;z-index:1;width:max-content;max-width:88%;margin:0;padding:.3em .75em;border-radius:8px;translate:-50% 0;
  background:rgba(9,9,11,.85);color:#fff;font-size:clamp(.8rem,2.6cqi,1.1rem);line-height:1.4;text-align:center;white-space:pre-line;pointer-events:none;
  transition:opacity .2s,bottom .3s}
@starting-style{.m8-cc{opacity:0}}
.m8[data-idle] .m8-cc{bottom:16px}
/* picture in picture: :has() swaps the empty frame for a note */
.m8:has([data-pip][aria-pressed="true"])::after{content:"Playing in picture in picture";position:absolute;inset:0;z-index:1;display:grid;place-items:center;
  background:#18181b;color:#d4d4d8;font-size:.9rem}
/* container queries: the same markup sheds controls as the player gets narrower */
@container m8 (width >= 600px){.m8-title{display:block}.m8 .m8-btn{width:42px;height:42px}}
@container m8 (width < 520px){.m8 .m8-skip,.m8-volgroup .m8-vol{display:none}.m8-cc{bottom:88px}}
@container m8 (width < 400px){.m8 .m8-speed,.m8-dur{display:none}.m8-bar{padding:24px 8px 6px}.m8 .m8-btn{width:34px;height:34px}}
@container m8 (width < 300px){.m8 [data-pip],.m8-time{display:none}}
@media (prefers-reduced-motion:reduce){.m8-bar,.m8-cc,.m8-volgroup .m8-vol{transition:none}}
JavaScript
(() => {
// Video player core. Each [data-vp] wraps one native <video>; its controls are found by data attributes
// (data-play, data-seek, data-skip, data-mute, data-vol, data-cc, data-speed, data-fs, data-pip, data-cur, data-dur),
// so a player only needs the controls it shows. Without JavaScript the video keeps its native controls.
// Shortcuts work while focus is inside the player: Space or K play/pause, Left/Right seek 5 s, M mute,
// F full screen, C captions.
const vpFmt = s => { s = Math.max(0, Math.floor(s || 0)); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
function vpPlayer(root) {
  const $ = s => root.querySelector(s);
  const video = $('video'), plays = [...root.querySelectorAll('[data-play]')];
  const seek = $('[data-seek]'), vol = $('[data-vol]'), mute = $('[data-mute]'), cc = $('[data-cc]'), ccBox = $('[data-cc-box]');
  const stage = $('[data-stage]') || root; // the element that goes full screen
  const speed = $('[data-speed]'), fs = $('[data-fs]'), pip = $('[data-pip]'), cur = $('[data-cur]'), dur = $('[data-dur]');
  const RATES = [1, 1.25, 1.5, 2, 0.5, 0.75], listeners = [];
  let raf = 0, idle = 0, track = null, pointer = 'mouse';
  video.controls = false;
  root.setAttribute('data-ready', '');

  const length = () => (isFinite(video.duration) && video.duration) || (seek ? +seek.max : 0);
  const sync = () => {
    const t = video.currentTime, d = length();
    root.style.setProperty('--p', d ? Math.min(t / d, 1) : 0);
    if (cur) cur.textContent = vpFmt(t);
    if (seek) {
      seek.value = t;
      const text = vpFmt(t) + ' of ' + vpFmt(d);
      if (seek.getAttribute('aria-valuetext') !== text) seek.setAttribute('aria-valuetext', text);
    }
    listeners.forEach(fn => fn(t, d));
  };
  const buffered = () => {
    const b = video.buffered, d = length();
    let end = 0;
    for (let i = 0; i < b.length; i++) if (b.start(i) <= video.currentTime + 0.5) end = Math.max(end, b.end(i));
    root.style.setProperty('--b', d ? Math.min(end / d, 1) : 0);
  };
  const tick = () => { sync(); raf = requestAnimationFrame(tick); };
  const meta = () => { if (seek) seek.max = length(); if (dur) dur.textContent = vpFmt(length()); sync(); buffered(); };
  const jump = t => { video.currentTime = Math.min(Math.max(t, 0), length()); sync(); wake(); };
  const start = () => {
    // one player at a time; muted background loops (data-bg) keep running
    document.querySelectorAll('video, audio').forEach(m => { if (m !== video && !m.hasAttribute('data-bg')) m.pause(); });
    return video.play().catch(() => {});
  };
  const toggle = () => (video.paused || video.ended ? start() : video.pause());
  const name = () => root.dataset.title || 'video';
  const state = () => {
    const playing = !video.paused;
    root.toggleAttribute('data-playing', playing);
    plays.forEach(b => b.setAttribute('aria-label', (playing ? 'Pause ' : 'Play ') + name()));
    // if the focused play button was just hidden (a big centre button), hand focus to a visible one
    const a = document.activeElement;
    if (plays.includes(a) && !a.checkVisibility?.()) plays.find(b => b.checkVisibility?.())?.focus();
  };

  // Captions: the track stays in "hidden" mode and the script prints the active cue into [data-cc-box],
  // so the text sits above the control bar. Players without a box fall back to native rendering.
  // A player without a captions button shows captions when the root has data-captions.
  const ccOn = () => (cc ? cc.getAttribute('aria-pressed') === 'true' : root.hasAttribute('data-captions'));
  const render = () => {
    if (!ccBox) return;
    const cues = ccOn() && track && track.activeCues ? [...track.activeCues] : [];
    const text = cues.map(c => c.text.replace(/<[^>]+>/g, '')).join('\n');
    ccBox.textContent = text;
    ccBox.hidden = !text;
  };
  const captions = () => {
    track = [...video.textTracks].find(t => t.kind === 'captions' || t.kind === 'subtitles') || null;
    if (cc) cc.hidden = !track;
    if (!track) return render();
    track.mode = ccBox || !ccOn() ? 'hidden' : 'showing';
    track.oncuechange = render;
    render();
  };

  // Controls fade out after 2.5 s of playback without pointer movement, unless keyboard focus is inside.
  const keyboardFocus = () => { const a = document.activeElement; return !!a && a !== root && root.contains(a) && a.matches(':focus-visible'); };
  function wake() {
    root.removeAttribute('data-idle');
    clearTimeout(idle);
    if (!root.hasAttribute('data-autohide') || video.paused) return;
    idle = setTimeout(() => { if (!video.paused && !keyboardFocus()) root.setAttribute('data-idle', ''); }, 2500);
  }

  const fsElement = () => document.fullscreenElement || document.webkitFullscreenElement;
  const fullscreen = () => {
    if (fsElement() === stage) (document.exitFullscreen || document.webkitExitFullscreen).call(document);
    else if (stage.requestFullscreen) stage.requestFullscreen().catch(() => {});
    else if (stage.webkitRequestFullscreen) stage.webkitRequestFullscreen();
    else if (video.webkitEnterFullscreen) video.webkitEnterFullscreen(); // iPhone: native player
  };
  const fsSync = () => {
    const on = fsElement() === stage;
    root.toggleAttribute('data-fullscreen', on);
    if (fs) fs.setAttribute('aria-label', on ? 'Exit full screen' : 'Full screen');
  };

  plays.forEach(b => b.addEventListener('click', toggle));
  root.querySelectorAll('[data-skip]').forEach(b => b.addEventListener('click', () => jump(video.currentTime + +b.dataset.skip)));
  root.addEventListener('pointerdown', e => { pointer = e.pointerType; }, true);
  video.addEventListener('click', () => {
    // a tap on hidden controls only reveals them; a click toggles playback and gives the player focus for shortcuts
    if (pointer !== 'mouse' && root.hasAttribute('data-idle')) return wake();
    toggle();
    if (!root.contains(document.activeElement)) root.focus({ preventScroll: true });
  });
  video.addEventListener('play', () => { state(); tick(); wake(); });
  video.addEventListener('pause', () => { state(); cancelAnimationFrame(raf); sync(); wake(); });
  video.addEventListener('loadedmetadata', () => { meta(); captions(); });
  video.addEventListener('progress', buffered);
  video.addEventListener('seeked', () => { sync(); buffered(); });
  video.addEventListener('volumechange', () => {
    const v = video.muted ? 0 : video.volume;
    root.style.setProperty('--v', v);
    if (vol) { vol.value = v; vol.setAttribute('aria-valuetext', Math.round(v * 100) + '%'); }
    if (mute) mute.setAttribute('aria-pressed', String(v === 0));
  });
  video.addEventListener('ratechange', () => {
    if (speed) (speed.querySelector('[data-speed-val]') || speed).textContent = video.playbackRate + '×';
  });
  video.textTracks.addEventListener('addtrack', captions);
  video.querySelectorAll('track').forEach(t => t.addEventListener('load', captions));
  if (seek) seek.addEventListener('input', () => jump(+seek.value));
  if (vol) vol.addEventListener('input', () => { video.volume = +vol.value; video.muted = +vol.value === 0; });
  if (mute) mute.addEventListener('click', () => {
    if (video.muted || video.volume === 0) { video.muted = false; if (video.volume === 0) video.volume = 0.6; }
    else video.muted = true;
  });
  if (cc) cc.addEventListener('click', () => {
    cc.setAttribute('aria-pressed', String(!ccOn()));
    if (track && !ccBox) track.mode = ccOn() ? 'showing' : 'hidden';
    render();
  });
  if (speed) speed.addEventListener('click', () => {
    const rate = RATES[(RATES.indexOf(video.playbackRate) + 1) % RATES.length];
    video.playbackRate = video.defaultPlaybackRate = rate;
  });
  if (fs) {
    fs.hidden = !(stage.requestFullscreen || stage.webkitRequestFullscreen || video.webkitEnterFullscreen);
    fs.addEventListener('click', fullscreen);
    document.addEventListener('fullscreenchange', fsSync);
    document.addEventListener('webkitfullscreenchange', fsSync);
  }
  if (pip) {
    pip.hidden = !(document.pictureInPictureEnabled && !video.disablePictureInPicture);
    pip.addEventListener('click', () => (document.pictureInPictureElement === video
      ? document.exitPictureInPicture() : video.requestPictureInPicture()).catch(() => {}));
    video.addEventListener('enterpictureinpicture', () => pip.setAttribute('aria-pressed', 'true'));
    video.addEventListener('leavepictureinpicture', () => pip.setAttribute('aria-pressed', 'false'));
  }
  ['pointermove', 'focusin', 'focusout'].forEach(t => root.addEventListener(t, wake));
  root.addEventListener('pointerleave', () => {
    if (!root.hasAttribute('data-autohide') || video.paused || keyboardFocus()) return;
    clearTimeout(idle);
    idle = setTimeout(() => root.setAttribute('data-idle', ''), 600);
  });
  root.addEventListener('keydown', e => {
    if (e.altKey || e.ctrlKey || e.metaKey || e.defaultPrevented) return;
    const t = e.target, k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
    const control = t.matches('button, a[href], select, textarea, input:not([type="range"])');
    wake();
    if (k === 'k' || (k === ' ' && !control)) toggle();
    else if (k === 'm') video.muted = !video.muted;
    else if (k === 'f' && fs && !fs.hidden) fullscreen();
    else if (k === 'c' && cc && !cc.hidden) cc.click();
    else if ((k === 'ArrowLeft' || k === 'ArrowRight') && (t === seek || !t.matches('input[type="range"]')))
      jump(video.currentTime + (k === 'ArrowLeft' ? -5 : 5));
    else return;
    e.preventDefault();
  });

  // load() swaps the source, poster and captions file (playlists and lightboxes use it)
  const load = ({ src, poster, captions: vtt, title, duration }) => {
    video.pause();
    video.querySelectorAll('track').forEach(t => t.remove());
    if (title) root.dataset.title = title;
    video.poster = poster || '';
    video.src = src;
    if (vtt) {
      const t = Object.assign(document.createElement('track'), { kind: 'captions', srclang: 'en', label: 'English', src: vtt });
      t.addEventListener('load', captions);
      video.append(t);
    }
    if (seek && duration) seek.max = duration;
    if (dur && duration) dur.textContent = vpFmt(duration);
    video.load();
    state(); sync(); captions();
  };

  if (video.readyState > 0) meta();
  captions(); state(); fsSync();
  video.dispatchEvent(new Event('volumechange'));
  return { root, video, jump, start, toggle, load, onTime: fn => listeners.push(fn) };
}

document.querySelectorAll('[data-vp="mo"]').forEach(vpPlayer);
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5c5c66] uppercase">Wide container</span>
<div class="group/vp @container/m8 relative isolate overflow-hidden rounded-[22px] bg-[#18181b] text-[#fafafa] shadow-[0_20px_44px_rgba(24,24,27,.22)] [--b:0] [--p:0] [--v:1] focus:outline-none data-fullscreen:rounded-none data-idle:not-has-[.m8-bar_:focus-visible]:cursor-none has-[[data-pip][aria-pressed=true]]:after:absolute has-[[data-pip][aria-pressed=true]]:after:inset-0 has-[[data-pip][aria-pressed=true]]:after:z-[1] has-[[data-pip][aria-pressed=true]]:after:grid has-[[data-pip][aria-pressed=true]]:after:place-items-center has-[[data-pip][aria-pressed=true]]:after:bg-[#18181b] has-[[data-pip][aria-pressed=true]]:after:text-[.9rem] has-[[data-pip][aria-pressed=true]]:after:text-[#d4d4d8] has-[[data-pip][aria-pressed=true]]:after:content-['Playing_in_picture_in_picture']" data-vp="mo" data-title="Fractal Tide" data-autohide role="group" aria-label="Fractal Tide video player" tabindex="-1">
  <video class="block aspect-video h-auto w-full bg-[#18181b] object-cover group-data-fullscreen/vp:h-full group-data-fullscreen/vp:object-contain" src="../media/clip-1.mp4" poster="../media/clip-1-poster.jpg" preload="metadata" playsinline controls>
    <track kind="captions" src="../media/clip-1-captions.vtt" srclang="en" label="English" default>
  </video>
  <p class="pointer-events-none absolute inset-x-0 top-0 z-[2] hidden bg-[linear-gradient(rgba(0,0,0,.6),transparent)] px-5 pt-4 pb-10 text-[clamp(.95rem,2.4cqi,1.3rem)] font-bold @min-[600px]/m8:group-data-ready/vp:block" aria-hidden="true">Fractal Tide</p>
  <p class="pointer-events-none absolute bottom-24 left-1/2 z-[1] m-0 w-max max-w-[88%] -translate-x-1/2 rounded-lg bg-[rgba(9,9,11,.85)] px-[.75em] py-[.3em] text-center text-[clamp(.8rem,2.6cqi,1.1rem)] leading-[1.4] whitespace-pre-line text-white transition-[opacity,bottom] duration-200 group-data-idle/vp:bottom-4 starting:opacity-0 motion-reduce:transition-none @max-[520px]/m8:bottom-[88px] @max-[520px]/m8:group-data-idle/vp:bottom-4" data-cc-box hidden></p>
  <div class="m8-bar absolute inset-x-0 bottom-0 z-[2] hidden bg-[linear-gradient(to_top,rgba(9,9,11,.9),rgba(9,9,11,.5)_60%,transparent)] px-3 pt-8 pb-2 transition-[opacity,translate,display] transition-discrete duration-300 group-data-ready/vp:block starting:translate-y-2.5 starting:opacity-0 group-data-idle/vp:not-has-[:focus-visible]:pointer-events-none group-data-idle/vp:not-has-[:focus-visible]:translate-y-2.5 group-data-idle/vp:not-has-[:focus-visible]:opacity-0 motion-reduce:transition-none @max-[400px]/m8:px-2 @max-[400px]/m8:pt-6 @max-[400px]/m8:pb-1.5">
    <input type="range" class="m8-range w-full" data-seek min="0" max="16" step="0.1" value="0" aria-label="Seek" aria-valuetext="0:00 of 0:16">
    <div class="mt-1.5 flex items-center gap-0.5">
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px]" data-play aria-label="Play Fractal Tide"><svg class="block size-[21px] group-data-playing/vp:hidden" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="hidden size-[21px] group-data-playing/vp:block" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] @max-[520px]/m8:hidden" data-skip="-10" aria-label="Back 10 seconds"><svg class="block size-[21px]" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3.5 4v4.5H8"/><text x="12.6" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] @max-[520px]/m8:hidden" data-skip="10" aria-label="Forward 10 seconds"><svg class="block size-[21px]" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 12a8.5 8.5 0 1 1-2.6-6.1"/><path d="M20.5 4v4.5H16"/><text x="11.4" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <div class="group/vol flex items-center">
        <button type="button" class="group/m relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px]" data-mute aria-label="Mute" aria-pressed="false"><svg class="block size-[21px] group-aria-pressed/m:hidden" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/></svg><svg class="hidden size-[21px] group-aria-pressed/m:block" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="m16 9 6 6m0-6-6 6"/></svg></button>
        <input type="range" class="m8-range w-0 flex-none opacity-0 transition-[width,opacity] duration-250 [--m8-f:var(--v)] [--m8-fb:0] group-hover/vol:w-[84px] group-hover/vol:opacity-100 group-has-[:focus-visible]/vol:w-[84px] group-has-[:focus-visible]/vol:opacity-100 group-has-[[data-mute][aria-pressed=true]]/vp:brightness-80 group-has-[[data-mute][aria-pressed=true]]/vp:grayscale motion-reduce:transition-none @max-[520px]/m8:hidden" data-vol min="0" max="1" step="0.05" value="1" aria-label="Volume" aria-valuetext="100%">
      </div>
      <span class="mr-auto px-1.5 text-[.82rem] whitespace-nowrap tabular-nums @max-[300px]/m8:hidden"><span data-cur>0:00</span><span class="@max-[400px]/m8:hidden"> / <span data-dur>0:16</span></span></span>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] w-auto! min-w-[46px] px-[9px] text-[.82rem] font-bold tabular-nums @max-[400px]/m8:hidden" data-speed><span class="sr-only">Playback speed </span><span data-speed-val>1×</span></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] aria-pressed:bg-[#a3e635] aria-pressed:text-[#18181b] aria-pressed:hover:bg-[#bef264] @max-[300px]/m8:ml-auto" data-cc aria-label="Captions" aria-pressed="true"><svg class="block size-[21px]" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><path d="M10.5 10.2a2.3 2.3 0 1 0 0 3.6M16.5 10.2a2.3 2.3 0 1 0 0 3.6"/></svg></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] aria-pressed:bg-[#a3e635] aria-pressed:text-[#18181b] aria-pressed:hover:bg-[#bef264] @max-[300px]/m8:hidden" data-pip aria-label="Picture in picture" aria-pressed="false" hidden><svg class="block size-[21px]" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2.5"/><rect x="12" y="11.5" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px]" data-fs aria-label="Full screen"><svg class="block size-[21px] group-data-fullscreen/vp:hidden" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg><svg class="hidden size-[21px] group-data-fullscreen/vp:block" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg></button>
    </div>
  </div>
</div>
</div>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5c5c66] uppercase">Narrow container, 320 pixels</span>
<div class="max-w-[320px]">
<div class="group/vp @container/m8 relative isolate overflow-hidden rounded-[22px] bg-[#18181b] text-[#fafafa] shadow-[0_20px_44px_rgba(24,24,27,.22)] [--b:0] [--p:0] [--v:1] focus:outline-none data-fullscreen:rounded-none data-idle:not-has-[.m8-bar_:focus-visible]:cursor-none has-[[data-pip][aria-pressed=true]]:after:absolute has-[[data-pip][aria-pressed=true]]:after:inset-0 has-[[data-pip][aria-pressed=true]]:after:z-[1] has-[[data-pip][aria-pressed=true]]:after:grid has-[[data-pip][aria-pressed=true]]:after:place-items-center has-[[data-pip][aria-pressed=true]]:after:bg-[#18181b] has-[[data-pip][aria-pressed=true]]:after:text-[.9rem] has-[[data-pip][aria-pressed=true]]:after:text-[#d4d4d8] has-[[data-pip][aria-pressed=true]]:after:content-['Playing_in_picture_in_picture']" data-vp="mo" data-title="Slow Gradients" data-autohide role="group" aria-label="Slow Gradients video player" tabindex="-1">
  <video class="block aspect-video h-auto w-full bg-[#18181b] object-cover group-data-fullscreen/vp:h-full group-data-fullscreen/vp:object-contain" src="../media/clip-4.mp4" poster="../media/clip-4-poster.jpg" preload="metadata" playsinline controls>
    <track kind="captions" src="../media/clip-4-captions.vtt" srclang="en" label="English" default>
  </video>
  <p class="pointer-events-none absolute inset-x-0 top-0 z-[2] hidden bg-[linear-gradient(rgba(0,0,0,.6),transparent)] px-5 pt-4 pb-10 text-[clamp(.95rem,2.4cqi,1.3rem)] font-bold @min-[600px]/m8:group-data-ready/vp:block" aria-hidden="true">Slow Gradients</p>
  <p class="pointer-events-none absolute bottom-24 left-1/2 z-[1] m-0 w-max max-w-[88%] -translate-x-1/2 rounded-lg bg-[rgba(9,9,11,.85)] px-[.75em] py-[.3em] text-center text-[clamp(.8rem,2.6cqi,1.1rem)] leading-[1.4] whitespace-pre-line text-white transition-[opacity,bottom] duration-200 group-data-idle/vp:bottom-4 starting:opacity-0 motion-reduce:transition-none @max-[520px]/m8:bottom-[88px] @max-[520px]/m8:group-data-idle/vp:bottom-4" data-cc-box hidden></p>
  <div class="m8-bar absolute inset-x-0 bottom-0 z-[2] hidden bg-[linear-gradient(to_top,rgba(9,9,11,.9),rgba(9,9,11,.5)_60%,transparent)] px-3 pt-8 pb-2 transition-[opacity,translate,display] transition-discrete duration-300 group-data-ready/vp:block starting:translate-y-2.5 starting:opacity-0 group-data-idle/vp:not-has-[:focus-visible]:pointer-events-none group-data-idle/vp:not-has-[:focus-visible]:translate-y-2.5 group-data-idle/vp:not-has-[:focus-visible]:opacity-0 motion-reduce:transition-none @max-[400px]/m8:px-2 @max-[400px]/m8:pt-6 @max-[400px]/m8:pb-1.5">
    <input type="range" class="m8-range w-full" data-seek min="0" max="12" step="0.1" value="0" aria-label="Seek" aria-valuetext="0:00 of 0:12">
    <div class="mt-1.5 flex items-center gap-0.5">
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px]" data-play aria-label="Play Slow Gradients"><svg class="block size-[21px] group-data-playing/vp:hidden" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="hidden size-[21px] group-data-playing/vp:block" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] @max-[520px]/m8:hidden" data-skip="-10" aria-label="Back 10 seconds"><svg class="block size-[21px]" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3.5 4v4.5H8"/><text x="12.6" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] @max-[520px]/m8:hidden" data-skip="10" aria-label="Forward 10 seconds"><svg class="block size-[21px]" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 12a8.5 8.5 0 1 1-2.6-6.1"/><path d="M20.5 4v4.5H16"/><text x="11.4" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <div class="group/vol flex items-center">
        <button type="button" class="group/m relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px]" data-mute aria-label="Mute" aria-pressed="false"><svg class="block size-[21px] group-aria-pressed/m:hidden" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/></svg><svg class="hidden size-[21px] group-aria-pressed/m:block" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="m16 9 6 6m0-6-6 6"/></svg></button>
        <input type="range" class="m8-range w-0 flex-none opacity-0 transition-[width,opacity] duration-250 [--m8-f:var(--v)] [--m8-fb:0] group-hover/vol:w-[84px] group-hover/vol:opacity-100 group-has-[:focus-visible]/vol:w-[84px] group-has-[:focus-visible]/vol:opacity-100 group-has-[[data-mute][aria-pressed=true]]/vp:brightness-80 group-has-[[data-mute][aria-pressed=true]]/vp:grayscale motion-reduce:transition-none @max-[520px]/m8:hidden" data-vol min="0" max="1" step="0.05" value="1" aria-label="Volume" aria-valuetext="100%">
      </div>
      <span class="mr-auto px-1.5 text-[.82rem] whitespace-nowrap tabular-nums @max-[300px]/m8:hidden"><span data-cur>0:00</span><span class="@max-[400px]/m8:hidden"> / <span data-dur>0:12</span></span></span>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] w-auto! min-w-[46px] px-[9px] text-[.82rem] font-bold tabular-nums @max-[400px]/m8:hidden" data-speed><span class="sr-only">Playback speed </span><span data-speed-val>1×</span></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] aria-pressed:bg-[#a3e635] aria-pressed:text-[#18181b] aria-pressed:hover:bg-[#bef264] @max-[300px]/m8:ml-auto" data-cc aria-label="Captions" aria-pressed="true"><svg class="block size-[21px]" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><path d="M10.5 10.2a2.3 2.3 0 1 0 0 3.6M16.5 10.2a2.3 2.3 0 1 0 0 3.6"/></svg></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] aria-pressed:bg-[#a3e635] aria-pressed:text-[#18181b] aria-pressed:hover:bg-[#bef264] @max-[300px]/m8:hidden" data-pip aria-label="Picture in picture" aria-pressed="false" hidden><svg class="block size-[21px]" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2.5"/><rect x="12" y="11.5" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px]" data-fs aria-label="Full screen"><svg class="block size-[21px] group-data-fullscreen/vp:hidden" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg><svg class="hidden size-[21px] group-data-fullscreen/vp:block" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg></button>
    </div>
  </div>
</div>
</div></div>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5c5c66] uppercase">Resizable container</span>
<div class="w-[560px] max-w-full min-w-[240px] resize-x overflow-hidden rounded-3xl border-2 border-dashed border-[#b4b4bd] p-2.5">
<div class="group/vp @container/m8 relative isolate overflow-hidden rounded-[22px] bg-[#18181b] text-[#fafafa] shadow-[0_20px_44px_rgba(24,24,27,.22)] [--b:0] [--p:0] [--v:1] focus:outline-none data-fullscreen:rounded-none data-idle:not-has-[.m8-bar_:focus-visible]:cursor-none has-[[data-pip][aria-pressed=true]]:after:absolute has-[[data-pip][aria-pressed=true]]:after:inset-0 has-[[data-pip][aria-pressed=true]]:after:z-[1] has-[[data-pip][aria-pressed=true]]:after:grid has-[[data-pip][aria-pressed=true]]:after:place-items-center has-[[data-pip][aria-pressed=true]]:after:bg-[#18181b] has-[[data-pip][aria-pressed=true]]:after:text-[.9rem] has-[[data-pip][aria-pressed=true]]:after:text-[#d4d4d8] has-[[data-pip][aria-pressed=true]]:after:content-['Playing_in_picture_in_picture']" data-vp="mo" data-title="Life in Colour" data-autohide role="group" aria-label="Life in Colour video player" tabindex="-1">
  <video class="block aspect-video h-auto w-full bg-[#18181b] object-cover group-data-fullscreen/vp:h-full group-data-fullscreen/vp:object-contain" src="../media/clip-3.mp4" poster="../media/clip-3-poster.jpg" preload="metadata" playsinline controls>
    <track kind="captions" src="../media/clip-3-captions.vtt" srclang="en" label="English" default>
  </video>
  <p class="pointer-events-none absolute inset-x-0 top-0 z-[2] hidden bg-[linear-gradient(rgba(0,0,0,.6),transparent)] px-5 pt-4 pb-10 text-[clamp(.95rem,2.4cqi,1.3rem)] font-bold @min-[600px]/m8:group-data-ready/vp:block" aria-hidden="true">Life in Colour</p>
  <p class="pointer-events-none absolute bottom-24 left-1/2 z-[1] m-0 w-max max-w-[88%] -translate-x-1/2 rounded-lg bg-[rgba(9,9,11,.85)] px-[.75em] py-[.3em] text-center text-[clamp(.8rem,2.6cqi,1.1rem)] leading-[1.4] whitespace-pre-line text-white transition-[opacity,bottom] duration-200 group-data-idle/vp:bottom-4 starting:opacity-0 motion-reduce:transition-none @max-[520px]/m8:bottom-[88px] @max-[520px]/m8:group-data-idle/vp:bottom-4" data-cc-box hidden></p>
  <div class="m8-bar absolute inset-x-0 bottom-0 z-[2] hidden bg-[linear-gradient(to_top,rgba(9,9,11,.9),rgba(9,9,11,.5)_60%,transparent)] px-3 pt-8 pb-2 transition-[opacity,translate,display] transition-discrete duration-300 group-data-ready/vp:block starting:translate-y-2.5 starting:opacity-0 group-data-idle/vp:not-has-[:focus-visible]:pointer-events-none group-data-idle/vp:not-has-[:focus-visible]:translate-y-2.5 group-data-idle/vp:not-has-[:focus-visible]:opacity-0 motion-reduce:transition-none @max-[400px]/m8:px-2 @max-[400px]/m8:pt-6 @max-[400px]/m8:pb-1.5">
    <input type="range" class="m8-range w-full" data-seek min="0" max="14" step="0.1" value="0" aria-label="Seek" aria-valuetext="0:00 of 0:14">
    <div class="mt-1.5 flex items-center gap-0.5">
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px]" data-play aria-label="Play Life in Colour"><svg class="block size-[21px] group-data-playing/vp:hidden" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="hidden size-[21px] group-data-playing/vp:block" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] @max-[520px]/m8:hidden" data-skip="-10" aria-label="Back 10 seconds"><svg class="block size-[21px]" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3.5 4v4.5H8"/><text x="12.6" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] @max-[520px]/m8:hidden" data-skip="10" aria-label="Forward 10 seconds"><svg class="block size-[21px]" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 12a8.5 8.5 0 1 1-2.6-6.1"/><path d="M20.5 4v4.5H16"/><text x="11.4" y="15.4" fill="currentColor" stroke="none" font-size="7" font-weight="700" text-anchor="middle" font-family="system-ui,sans-serif">10</text></svg></button>
      <div class="group/vol flex items-center">
        <button type="button" class="group/m relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px]" data-mute aria-label="Mute" aria-pressed="false"><svg class="block size-[21px] group-aria-pressed/m:hidden" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/></svg><svg class="hidden size-[21px] group-aria-pressed/m:block" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path d="m16 9 6 6m0-6-6 6"/></svg></button>
        <input type="range" class="m8-range w-0 flex-none opacity-0 transition-[width,opacity] duration-250 [--m8-f:var(--v)] [--m8-fb:0] group-hover/vol:w-[84px] group-hover/vol:opacity-100 group-has-[:focus-visible]/vol:w-[84px] group-has-[:focus-visible]/vol:opacity-100 group-has-[[data-mute][aria-pressed=true]]/vp:brightness-80 group-has-[[data-mute][aria-pressed=true]]/vp:grayscale motion-reduce:transition-none @max-[520px]/m8:hidden" data-vol min="0" max="1" step="0.05" value="1" aria-label="Volume" aria-valuetext="100%">
      </div>
      <span class="mr-auto px-1.5 text-[.82rem] whitespace-nowrap tabular-nums @max-[300px]/m8:hidden"><span data-cur>0:00</span><span class="@max-[400px]/m8:hidden"> / <span data-dur>0:14</span></span></span>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] w-auto! min-w-[46px] px-[9px] text-[.82rem] font-bold tabular-nums @max-[400px]/m8:hidden" data-speed><span class="sr-only">Playback speed </span><span data-speed-val>1×</span></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] aria-pressed:bg-[#a3e635] aria-pressed:text-[#18181b] aria-pressed:hover:bg-[#bef264] @max-[300px]/m8:ml-auto" data-cc aria-label="Captions" aria-pressed="true"><svg class="block size-[21px]" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><path d="M10.5 10.2a2.3 2.3 0 1 0 0 3.6M16.5 10.2a2.3 2.3 0 1 0 0 3.6"/></svg></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px] aria-pressed:bg-[#a3e635] aria-pressed:text-[#18181b] aria-pressed:hover:bg-[#bef264] @max-[300px]/m8:hidden" data-pip aria-label="Picture in picture" aria-pressed="false" hidden><svg class="block size-[21px]" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2.5"/><rect x="12" y="11.5" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg></button>
      <button type="button" class="relative grid size-[38px] flex-none cursor-pointer place-items-center rounded-xl hover:bg-white/14 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#a3e635] @min-[600px]/m8:size-[42px] @max-[400px]/m8:size-[34px]" data-fs aria-label="Full screen"><svg class="block size-[21px] group-data-fullscreen/vp:hidden" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg><svg class="hidden size-[21px] group-data-fullscreen/vp:block" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg></button>
    </div>
  </div>
</div>
</div>
<p class="mt-2.5 text-[.8rem] text-[#5c5c66]">Drag the bottom-right corner of the dashed box: the control bar changes with the box, not the window.</p>
</div>
CSS
/* Range inputs need vendor pseudo-elements, so they get one small utility. --f is the filled share
   (defaults to --p, the played share) and --fb the buffered share; both are set by the script.
   Everything else is utilities: @container/m8 with @max-[520px]/m8: variants, has-[...] for :has(),
   starting: for @starting-style and transition-discrete for the display transition. */
@utility m8-range {
  -webkit-appearance: none; appearance: none; display: block; min-width: 0; height: 16px; margin: 0; background: none; cursor: pointer;
  --f: var(--m8-f, var(--p)); --fb: var(--m8-fb, var(--b));
  --fill: linear-gradient(to right, #a3e635 0 calc(7px + (100% - 14px) * var(--f)), rgb(255 255 255 / .6) 0 calc(7px + (100% - 14px) * var(--fb)), rgb(255 255 255 / .35) 0);
  &:focus-visible { outline: none; }
  &::-webkit-slider-runnable-track { height: 4px; border-radius: 99px; background: var(--fill); }
  &::-moz-range-track { height: 4px; border-radius: 99px; background: var(--fill); }
  &::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 14px; height: 14px; margin-top: -5px; border: 0; border-radius: 50%; background: #fff; }
  &::-moz-range-thumb { width: 14px; height: 14px; border: 0; border-radius: 50%; background: #fff; }
  &:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px #18181b, 0 0 0 5px #a3e635; }
  &:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 3px #18181b, 0 0 0 5px #a3e635; }
}
JavaScript
(() => {
// Video player core. Each [data-vp] wraps one native <video>; its controls are found by data attributes
// (data-play, data-seek, data-skip, data-mute, data-vol, data-cc, data-speed, data-fs, data-pip, data-cur, data-dur),
// so a player only needs the controls it shows. Without JavaScript the video keeps its native controls.
// Shortcuts work while focus is inside the player: Space or K play/pause, Left/Right seek 5 s, M mute,
// F full screen, C captions.
const vpFmt = s => { s = Math.max(0, Math.floor(s || 0)); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
function vpPlayer(root) {
  const $ = s => root.querySelector(s);
  const video = $('video'), plays = [...root.querySelectorAll('[data-play]')];
  const seek = $('[data-seek]'), vol = $('[data-vol]'), mute = $('[data-mute]'), cc = $('[data-cc]'), ccBox = $('[data-cc-box]');
  const stage = $('[data-stage]') || root; // the element that goes full screen
  const speed = $('[data-speed]'), fs = $('[data-fs]'), pip = $('[data-pip]'), cur = $('[data-cur]'), dur = $('[data-dur]');
  const RATES = [1, 1.25, 1.5, 2, 0.5, 0.75], listeners = [];
  let raf = 0, idle = 0, track = null, pointer = 'mouse';
  video.controls = false;
  root.setAttribute('data-ready', '');

  const length = () => (isFinite(video.duration) && video.duration) || (seek ? +seek.max : 0);
  const sync = () => {
    const t = video.currentTime, d = length();
    root.style.setProperty('--p', d ? Math.min(t / d, 1) : 0);
    if (cur) cur.textContent = vpFmt(t);
    if (seek) {
      seek.value = t;
      const text = vpFmt(t) + ' of ' + vpFmt(d);
      if (seek.getAttribute('aria-valuetext') !== text) seek.setAttribute('aria-valuetext', text);
    }
    listeners.forEach(fn => fn(t, d));
  };
  const buffered = () => {
    const b = video.buffered, d = length();
    let end = 0;
    for (let i = 0; i < b.length; i++) if (b.start(i) <= video.currentTime + 0.5) end = Math.max(end, b.end(i));
    root.style.setProperty('--b', d ? Math.min(end / d, 1) : 0);
  };
  const tick = () => { sync(); raf = requestAnimationFrame(tick); };
  const meta = () => { if (seek) seek.max = length(); if (dur) dur.textContent = vpFmt(length()); sync(); buffered(); };
  const jump = t => { video.currentTime = Math.min(Math.max(t, 0), length()); sync(); wake(); };
  const start = () => {
    // one player at a time; muted background loops (data-bg) keep running
    document.querySelectorAll('video, audio').forEach(m => { if (m !== video && !m.hasAttribute('data-bg')) m.pause(); });
    return video.play().catch(() => {});
  };
  const toggle = () => (video.paused || video.ended ? start() : video.pause());
  const name = () => root.dataset.title || 'video';
  const state = () => {
    const playing = !video.paused;
    root.toggleAttribute('data-playing', playing);
    plays.forEach(b => b.setAttribute('aria-label', (playing ? 'Pause ' : 'Play ') + name()));
    // if the focused play button was just hidden (a big centre button), hand focus to a visible one
    const a = document.activeElement;
    if (plays.includes(a) && !a.checkVisibility?.()) plays.find(b => b.checkVisibility?.())?.focus();
  };

  // Captions: the track stays in "hidden" mode and the script prints the active cue into [data-cc-box],
  // so the text sits above the control bar. Players without a box fall back to native rendering.
  // A player without a captions button shows captions when the root has data-captions.
  const ccOn = () => (cc ? cc.getAttribute('aria-pressed') === 'true' : root.hasAttribute('data-captions'));
  const render = () => {
    if (!ccBox) return;
    const cues = ccOn() && track && track.activeCues ? [...track.activeCues] : [];
    const text = cues.map(c => c.text.replace(/<[^>]+>/g, '')).join('\n');
    ccBox.textContent = text;
    ccBox.hidden = !text;
  };
  const captions = () => {
    track = [...video.textTracks].find(t => t.kind === 'captions' || t.kind === 'subtitles') || null;
    if (cc) cc.hidden = !track;
    if (!track) return render();
    track.mode = ccBox || !ccOn() ? 'hidden' : 'showing';
    track.oncuechange = render;
    render();
  };

  // Controls fade out after 2.5 s of playback without pointer movement, unless keyboard focus is inside.
  const keyboardFocus = () => { const a = document.activeElement; return !!a && a !== root && root.contains(a) && a.matches(':focus-visible'); };
  function wake() {
    root.removeAttribute('data-idle');
    clearTimeout(idle);
    if (!root.hasAttribute('data-autohide') || video.paused) return;
    idle = setTimeout(() => { if (!video.paused && !keyboardFocus()) root.setAttribute('data-idle', ''); }, 2500);
  }

  const fsElement = () => document.fullscreenElement || document.webkitFullscreenElement;
  const fullscreen = () => {
    if (fsElement() === stage) (document.exitFullscreen || document.webkitExitFullscreen).call(document);
    else if (stage.requestFullscreen) stage.requestFullscreen().catch(() => {});
    else if (stage.webkitRequestFullscreen) stage.webkitRequestFullscreen();
    else if (video.webkitEnterFullscreen) video.webkitEnterFullscreen(); // iPhone: native player
  };
  const fsSync = () => {
    const on = fsElement() === stage;
    root.toggleAttribute('data-fullscreen', on);
    if (fs) fs.setAttribute('aria-label', on ? 'Exit full screen' : 'Full screen');
  };

  plays.forEach(b => b.addEventListener('click', toggle));
  root.querySelectorAll('[data-skip]').forEach(b => b.addEventListener('click', () => jump(video.currentTime + +b.dataset.skip)));
  root.addEventListener('pointerdown', e => { pointer = e.pointerType; }, true);
  video.addEventListener('click', () => {
    // a tap on hidden controls only reveals them; a click toggles playback and gives the player focus for shortcuts
    if (pointer !== 'mouse' && root.hasAttribute('data-idle')) return wake();
    toggle();
    if (!root.contains(document.activeElement)) root.focus({ preventScroll: true });
  });
  video.addEventListener('play', () => { state(); tick(); wake(); });
  video.addEventListener('pause', () => { state(); cancelAnimationFrame(raf); sync(); wake(); });
  video.addEventListener('loadedmetadata', () => { meta(); captions(); });
  video.addEventListener('progress', buffered);
  video.addEventListener('seeked', () => { sync(); buffered(); });
  video.addEventListener('volumechange', () => {
    const v = video.muted ? 0 : video.volume;
    root.style.setProperty('--v', v);
    if (vol) { vol.value = v; vol.setAttribute('aria-valuetext', Math.round(v * 100) + '%'); }
    if (mute) mute.setAttribute('aria-pressed', String(v === 0));
  });
  video.addEventListener('ratechange', () => {
    if (speed) (speed.querySelector('[data-speed-val]') || speed).textContent = video.playbackRate + '×';
  });
  video.textTracks.addEventListener('addtrack', captions);
  video.querySelectorAll('track').forEach(t => t.addEventListener('load', captions));
  if (seek) seek.addEventListener('input', () => jump(+seek.value));
  if (vol) vol.addEventListener('input', () => { video.volume = +vol.value; video.muted = +vol.value === 0; });
  if (mute) mute.addEventListener('click', () => {
    if (video.muted || video.volume === 0) { video.muted = false; if (video.volume === 0) video.volume = 0.6; }
    else video.muted = true;
  });
  if (cc) cc.addEventListener('click', () => {
    cc.setAttribute('aria-pressed', String(!ccOn()));
    if (track && !ccBox) track.mode = ccOn() ? 'showing' : 'hidden';
    render();
  });
  if (speed) speed.addEventListener('click', () => {
    const rate = RATES[(RATES.indexOf(video.playbackRate) + 1) % RATES.length];
    video.playbackRate = video.defaultPlaybackRate = rate;
  });
  if (fs) {
    fs.hidden = !(stage.requestFullscreen || stage.webkitRequestFullscreen || video.webkitEnterFullscreen);
    fs.addEventListener('click', fullscreen);
    document.addEventListener('fullscreenchange', fsSync);
    document.addEventListener('webkitfullscreenchange', fsSync);
  }
  if (pip) {
    pip.hidden = !(document.pictureInPictureEnabled && !video.disablePictureInPicture);
    pip.addEventListener('click', () => (document.pictureInPictureElement === video
      ? document.exitPictureInPicture() : video.requestPictureInPicture()).catch(() => {}));
    video.addEventListener('enterpictureinpicture', () => pip.setAttribute('aria-pressed', 'true'));
    video.addEventListener('leavepictureinpicture', () => pip.setAttribute('aria-pressed', 'false'));
  }
  ['pointermove', 'focusin', 'focusout'].forEach(t => root.addEventListener(t, wake));
  root.addEventListener('pointerleave', () => {
    if (!root.hasAttribute('data-autohide') || video.paused || keyboardFocus()) return;
    clearTimeout(idle);
    idle = setTimeout(() => root.setAttribute('data-idle', ''), 600);
  });
  root.addEventListener('keydown', e => {
    if (e.altKey || e.ctrlKey || e.metaKey || e.defaultPrevented) return;
    const t = e.target, k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
    const control = t.matches('button, a[href], select, textarea, input:not([type="range"])');
    wake();
    if (k === 'k' || (k === ' ' && !control)) toggle();
    else if (k === 'm') video.muted = !video.muted;
    else if (k === 'f' && fs && !fs.hidden) fullscreen();
    else if (k === 'c' && cc && !cc.hidden) cc.click();
    else if ((k === 'ArrowLeft' || k === 'ArrowRight') && (t === seek || !t.matches('input[type="range"]')))
      jump(video.currentTime + (k === 'ArrowLeft' ? -5 : 5));
    else return;
    e.preventDefault();
  });

  // load() swaps the source, poster and captions file (playlists and lightboxes use it)
  const load = ({ src, poster, captions: vtt, title, duration }) => {
    video.pause();
    video.querySelectorAll('track').forEach(t => t.remove());
    if (title) root.dataset.title = title;
    video.poster = poster || '';
    video.src = src;
    if (vtt) {
      const t = Object.assign(document.createElement('track'), { kind: 'captions', srclang: 'en', label: 'English', src: vtt });
      t.addEventListener('load', captions);
      video.append(t);
    }
    if (seek && duration) seek.max = duration;
    if (dur && duration) dur.textContent = vpFmt(duration);
    video.load();
    state(); sync(); captions();
  };

  if (video.readyState > 0) meta();
  captions(); state(); fsSync();
  video.dispatchEvent(new Event('volumechange'));
  return { root, video, jump, start, toggle, load, onTime: fn => listeners.push(fn) };
}

document.querySelectorAll('[data-vp="mo"]').forEach(vpPlayer);
})();

About this set

One player markup that does in CSS what most player libraries do in JavaScript, shown in a wide container, a 320 pixel column and a resizable box. The player is a size container, so its control bar answers to its own width rather than the viewport: below 520 pixels the skip buttons and volume slider drop out, below 400 pixels the speed button and total time go, and below 300 pixels the picture-in-picture button and clock go too, while above 600 pixels a title overlay appears and buttons grow. The :has() selector reacts to control state without extra classes: the bar stays visible while one of its controls holds keyboard focus, the volume slider grows out of the mute button on hover or focus and dims while muted, and the frame shows a note while the video plays in picture in picture. @starting-style fades the control bar in when the player is ready and fades captions in as each cue appears. A picture-in-picture button is shown only where the browser supports the API. A short script still connects the controls to the video element.

What’s included

  • Control bar layout driven by container queries on the player itself
  • :has() keeps controls visible on keyboard focus and reveals volume
  • @starting-style entry fades for the control bar and captions
  • Picture-in-picture button shown only where supported
  • Same markup in a wide, a narrow and a resizable container

Accessibility

Every control is a named button or range input; the captions and picture-in-picture buttons use aria-pressed and the seek slider announces text such as 0:05 of 0:16. At small widths the skip buttons and volume slider drop out, but the arrow keys still seek and M still mutes while the player has focus. Focus rings are lime against the dark bar, and all fades and the volume reveal are removed under prefers-reduced-motion.

Customise it

Change --m8-accent on .m8 for the seek fill, pressed buttons and focus rings, and the breakpoints in the @container m8 rules to decide which controls drop out. Bootstrap restyles .btn and .form-range in the same stylesheet; in Tailwind edit the @max-[520px]/m8: style variants and the m8-range utility.