Media Player

Controls skin over a native <audio> / <video> element. letbe-ds owns the controls and layout; the playback engine stays native. Need HLS / DASH / DRM? Attach hls.js or shaka-player to the same media element — we never bundle a streaming engine. Every colour comes from existing design tokens (scrubber → bg-accent, buffered → bg-bolder, chrome → surface-bg-elevated).

a11y (built in): the engine stays native and so do the controls — scrubber and volume are <input type="range"> with aria-label and a :focus-visible ring on the thumb, subtitles are native <track kind="subtitles"> behind a CC language picker (Off plus every loaded track, rebuilt whenever a track is added). LB.Media rewrites accessible names as state changes (Play↔Pause, Mute↔Unmute, Fullscreen↔Exit fullscreen, "Playback speed 1.5x") and drives aria-pressed on the toggles (like/dislike, captions, PiP), aria-expanded on the playlist toggle, and aria-current="true" on the playing track. The player root takes tabindex="0" and owns the shortcut layer — ←/→ seek ±5s and ↑/↓ change volume from any control inside it (they preventDefault so a focused range cannot also move), Space/K plays unless a button has focus, M mutes; reorder grips are aria-hidden, and prefers-reduced-motion stops the title marquee and the inline track-panel transition.

Usage: Load js/lb.js, then js/components/lb-media.js — it self-registers and every [data-lb-media] root auto-inits as el._lbMedia (LB.Media). You author the markup: a .lb-media wrapper holding the native <audio> / <video> plus a .lb-media__controls row whose controls the wire-up finds by attribute — data-lb-media-playpause, -seek, -current / -duration, -mute / -volume, and opt-ins (-skip, -speed, -prev / -next, -pip, -fullscreen, -playlist) — copy the full class combos from a demo; whatever you omit simply isn't wired. Playback state travels as the native media events on the inner element (play, timeupdate, ended…); the player's own events are lb-media-quality-change (bubbles, detail.quality / detail.src) and lb-menu-select from its menus. Drive it via el._lbMedia.play() / pause() / toggle(), seekTo(sec), setRate(r), loadTrack(i, autoplay), prevTrack() / nextTrack(), setLoop(on), toggleCaptions(), togglePictureInPicture(), toggleFullscreen().

Demo audio is the Letbe Design sessions — original music, used with permission of the author.


Audio

The minimum pattern: data-lb-media on a wrapper containing a native <audio> + a .lb-media__controls row. The controller wires play/pause, seek (with buffered region), time, volume, and mute. Keyboard: focus any control inside the player, then Space/K (play), ←/→ (seek ±5s), ↑/↓ (volume), M (mute).

0:00
0:00

Speed & skip

Optional transport controls. Skip buttons use data-lb-media-skip="-10" / "10" (seconds, relative). The speed control (data-lb-media-speed) cycles the playback rate 0.5×–2× and shows the current value. Drop either anywhere in the controls row.

0:00
0:00

With album art & playlist

Album art and now-playing title are opt-in — add a .lb-media__now block with data-lb-media-art (an <img>) and data-lb-media-title / data-lb-media-artist text. Omit the block for a controls-only player (above). The playlist is a data-lb-media-playlist list of data-lb-media-track buttons carrying data-src / data-title; clicking loads + plays + highlights, and the now-playing block updates. Playback auto-advances on track end. Repeat-all is opt-in via data-lb-media-loop on the root (this demo has it) — when the last track ends, playback wraps back to track 1; Next on the last track wraps too. Runtime toggle: el._lbMedia.setLoop(true|false).

Hover any row — the duration fades and per-row actions appear: favourite, share, and a three-dots menu (uses LB.Menu + the shared .lb-list primitive) with playlist actions (Add to playlist, Remove from playlist) and links (Go to album / artist, Copy link, Download). Actions are opt-in: add a .lb-media__track-actions sibling to each track button. (Injected at runtime in this demo for brevity.) This playlist also uses inline actions mode (data-lb-media-playlist-actions-mode="inline") — clicking the three-dots expands a panel below the row instead of popping a dropdown, so it never overflows the container and works cleanly on touch. Default mode is dropdown.

Letbe Design — cover art
Old School Letbe Design
0:00
0:00

Compact — play on cover, single-line transport

The --compact layout overlays the play button on the cover and keeps the transport on one line: current time · scrubber · total time. (The play button can live anywhere in the player — the controller finds it by data attribute — so here it sits inside the cover.)

Also shown: title truncation + hover reveal. In a narrow layout both the now-playing title and the playlist row titles truncate with an ellipsis. Hover a title (or a playlist row) and it slides left to reveal the rest, then slides back on leave — one marquee mechanism the player applies everywhere (playlist titles are auto-wrapped, so the markup stays plain). Uses the design system's --lb-ease-linear at a distance-scaled multiple of --lb-duration-500 (longer titles scroll for longer, so the speed stays constant). Respects prefers-reduced-motion; full title also exposed as a native tooltip.

Letbe Design — cover art
Country Letbe Design
0:00
0:00

Card — now-playing layout

A focal "now-playing" arrangement: add lb-media--card. Same markup and controls as any player — the variant just rearranges them into two stacked rows: large cover on top, centred title + artist, a progress row (current time · scrubber · total time), then a transport row (previous · play · next).

Art ratio — square is the default; root modifiers mirror the card component's media ratio set: lb-media--art-16x9 · lb-media--art-4x3 · lb-media--art-1x1 · lb-media--art-3x4. Landscape ratios span the card width (banner/thumb treatment); square and portrait keep the focal cap. The corner actions sit on a translucent scrim pill (same recipe as the video scrims) so they stay readable over any artwork.

Letbe Design — cover art
Old School Letbe Design
0:00
0:00
Letbe Design — cover art
Pop Letbe Design
0:00
0:00

Video

When the player wraps a <video> instead of <audio>, the --video layout is applied automatically: the video fills the frame, scrims top + bottom render light controls that read on any footage, and the engine stays native — same controller as audio.

Demo features: top bar with like / dislike (left) and speed · captions · share · more (right); bottom transport with scrubber, mute, volume (half-width), Picture-in-Picture (data-lb-media-pip), and fullscreen; video playlist below the controls with drag-to-reorder (grip handle appears on row hover; quick clicks still play the track) and a scrollable list (.lb-media__playlist--scroll). Native HTML5 subtitles via <track kind="subtitles"> — the CC button is a language picker (Off + every available track); the trigger lights up while a track is showing. Upload subtitles in the three-dots menu adds a new track and it appears in the CC picker automatically.

Hold-to-skip (opt-in via data-lb-media-hold-to-skip): short click toggles play/pause; hold the left half for ~0.5s to start skipping backward in 10s steps, right half to skip forward. A small badge appears on the active side and accumulates the total skipped (10s ⟪ / ⟫ 10s). Idle auto-hide still applies during playback.

Quality picker — plain multi-source switching: declare one file per quality in data-lb-media-qualities (JSON of {label, src}) and add a data-lb-media-quality menu; switching preserves position, speed and play state (this demo ships 720p + 480p). For adaptive bitrate streaming (auto-switching 1080p/2K/4K ladders) attach hls.js or shaka-player to the same <video> — per the pluggable-engines rule the DS ships the picker chrome, you bring the streaming engine.

      0:00
      0:00