v1.2.0
  1. ds
  2. Patterns
  3. Playback bar

Playback bar

Play, scrub, time and speed for anything on a clock.

0:00.0 / 0:12.0
Show code
<div class="playbar">
  <button class="btn icon sm accent" aria-label="Play"><svg class="i">…</svg></button>
  <div class="scrub" style="--v:.35">
    <input class="range" type="range" min="0" max="12000" step="10" value="4200"
           aria-label="Position" aria-valuetext="4.2 seconds of 12" />
    <div class="scrub-marks" aria-hidden="true">
      <i class="on" style="--at:.06"></i>
      <i class="on" style="--at:.19"></i>
      <i style="--at:.47"></i>
      <i class="down" style="--at:.61"></i>
    </div>
  </div>
  <span class="tm"><b>0:04.2</b> / 0:12.0</span>
  <div class="seg sm" role="group" aria-label="Speed">
    <button aria-pressed="false">0.5×</button>
    <button aria-pressed="true">1×</button>
    <button aria-pressed="false">2×</button>
  </div>
</div>

ack's transport, for anything that plays on a clock: a play button, a scrubber over the real time axis, the time, and the speed. Markers are events on that axis, in ack the messages of a sequence diagram. They turn purple once played; a red one is a message that fails. Press play, drag the thumb, or focus it and use the arrow keys.

The play button is the one accent button on the screen, because playing is what the screen is for. On phones the scrubber takes a row of its own above the controls.

Parts

The scrubber is a range whose track is drawn by .scrub instead, so the markers can sit between the fill and the thumb. Set --v (0 to 1) on .scrub and --at (0 to 1) on each marker. The marker layer is inset by half a thumb, the same as the thumb's travel, so a marker at --at: .5 is exactly under the thumb at the halfway point.

// One clock, one source of truth. The range only mirrors it.
let t = 0, speed = 1, last = 0, raf = 0;
const tick = (now) => {
  t = Math.min(total, t + (now - last) * speed);
  last = now;
  range.value = t;
  scrub.style.setProperty('--v', t / total);
  marks.forEach((m) => m.classList.toggle('on', m.at * total <= t));
  time.textContent = fmt(t);
  raf = t < total ? requestAnimationFrame(tick) : 0;
};
range.addEventListener('input', () => { t = +range.value; tick(last = performance.now()); });

API

ClassOnWhat it does
.playbardivThe bar: surface, hairline, 8px radius. Wraps on phones with the scrubber on top.
.scrubdivWraps a .range and draws its track and fill from --v.
.scrub-marksdivThe marker layer. Children are <i> with --at from 0 to 1.
.on / .warn / .down.scrub-marks > iPlayed, degraded, failed.
.tmspanMono time readout with tabular figures. The current time goes in <b>.
.seg.smdivThe speed control. See segmented control.

Accessibility

  • The play button swaps its aria-label between Play and Pause along with the icon.
  • The scrubber is a real range: give it aria-label="Position" and keep aria-valuetext in words, "4.2 seconds of 12".
  • With a fine step (10 ms here) the arrow keys would crawl, so the demo moves 0.5 s per press and 2 s with Shift.
  • If the page has one player, bind Space to play and pause while focus is not in a text field.