Skip to main content

Core · Media

Player

Plays a video, a sound or a MIDI file in the page, with the same controls for all three.

Example

A video and an audio file with the same controls.

0:00 0:00

Video, MP4, 15 seconds, with English captions shown by default and Spanish subtitles. It has no sound: both tracks describe the picture.

Video: NASA, Earth Views from the ISS, in the public domain.

0:00 0:00

Audio, MP3, 25 seconds.

Nothing is downloaded until you press play. Recording: NASA, Apollo 11 launch countdown and lift-off, in the public domain.

MIDI playback needs JavaScript.

0:00 0:00

MIDI, parsed and played with Web Audio by midi.js, which loads only when a MIDI player is on the page.

Known issues

Captions, transcripts and audio description depend on the media you provide. The colours of the captions are a request: the reader's own caption settings win, and a browser may draw them its own way, in its own full-screen player for example.

A MIDI file has no player without JavaScript: offer a download link in the fallback text.

Code

Copy the markup

An audio player. The video player adds a .luz-player-media-wrap around the <video> and a mute button and volume slider: copy it from the example above.

HTML
<div class="luz-player" data-type="audio" role="group" aria-label="Episode 12 player">
  <audio class="luz-player-audio" preload="none" controls>
    <source src="episode-12.mp3" type="audio/mpeg">
  </audio>
  <div class="luz-player-controls luz-player-controls--audio">
    <button class="luz-player-btn luz-player-playpause" type="button" aria-label="Play">
      <svg class="icon-play" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4.5v15l12-7.5L7 4.5Z"/></g></svg>
      <svg class="icon-pause" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" style="display:none"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></g></svg>
    </button>
    <div class="luz-player-progress-row luz-player-progress-row--audio">
      <span class="luz-player-time luz-player-current">0:00</span>
      <input class="luz-player-seekbar" type="range" min="0" max="1000" step="1" value="0" aria-label="Seek audio">
      <span class="luz-player-time luz-player-duration">0:00</span>
    </div>
  </div>
</div>

Captions and subtitles: one <track> per file inside the <video>, and one control in .luz-player-btns. Use the button for one track and the select for several; the script fills the select.

HTML
<video class="luz-player-video" preload="metadata" controls playsinline>
  <source src="interview.mp4" type="video/mp4">
  <track kind="captions" srclang="en" label="English" src="interview-en.vtt" default>
  <track kind="subtitles" srclang="es" label="Español" src="interview-es.vtt">
</video>

<!-- One track: a button that turns it on and off -->
<button class="luz-player-btn luz-player-captions" type="button" aria-pressed="false" aria-label="Captions">
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M6 11h12M6 15h7"/></svg>
</button>

<!-- Several tracks: a select, filled by the script -->
<select class="luz-input luz-player-tracks" aria-label="Captions"></select>
JS
import { initPlayers } from "@luz-ds/core/player";
initPlayers();

Variants and options

Class or attributeEffect
data-type="video"
A <video class="luz-player-video">; a click on the picture plays or pauses
data-type="audio"
An <audio class="luz-player-audio"> with a compact control row (.luz-player-controls--audio)
data-type="midi" and data-src
A MIDI file, played with Web Audio by midi.js, loaded only when needed
.luz-player-mute, .luz-player-volbar
A mute button and a volume slider
.luz-player-fullscreen, .luz-player-pip
Optional full-screen and picture-in-picture buttons for video
<track kind="captions">, kind="subtitles"
A WebVTT file with srclang and label; default shows it from the start
.luz-player-captions
A button that turns captions on and off: the default track, else the first
.luz-player-tracks
A select, with .luz-input, to pick one track or none
.luz-player-fallback
Text shown only when the script does not run

How it works

The native media element plays the file; player.js drives the LUZ controls, styled by components/player.css.

  • initPlayers() mounts every .luz-player on the page, or inside the element you pass it. luz.js runs it. A player already mounted is left as it is, so running it again is safe.
  • On mount, the script removes the native controls and binds the LUZ ones: play and pause, time, seek and volume. They start from the media's own state: a muted video shows as muted, and a length the browser already knows shows at once.
  • Seek and volume are native <input type="range"> sliders. Once the length is known, the seek slider has one step per second, whatever its max in the markup.
  • A full-screen or picture-in-picture button is hidden where the browser cannot do it.
  • Captions and subtitles are the browser's: each <track> is a text track, and the browser draws its cues. The LUZ controls only set which track shows, so a choice made in the browser's own menu shows in them too. ::cue gives the cues light text on the media surface. Nothing is stored. A control with no track to show is hidden.
  • mountPlayer(root) and mountMidiPlayer(root) mount one player.
  • The player is a size-query container: below 480 px of its own width the audio controls stack and the volume bar is hidden, wherever the player is placed. A parent that sizes it to its content (an inline-flex item, a float) needs a width of its own.
  • In right-to-left text the timeline follows the reading direction: the seek bar starts at the right and fills towards the left, the controls row mirrors, and the slider's keys are the browser's own. The play, pause and volume icons are not mirrored: they do not point anywhere.

Without JavaScript

Audio and video show the browser's own controls, and the LUZ controls stay hidden. The browser's controls offer the captions and subtitles, and a track with default shows from the start.

A MIDI player shows its fallback text instead.

Keyboard and ARIA

Tab moves through the controls; each one keeps its native keys.

  • Buttons: Enter or Space. Their names change with the state: "Play" or "Pause", "Mute" or "Unmute". The captions button keeps its name, "Captions", and says whether it is pressed (aria-pressed).
  • Track select: the browser's own keys; each option is in the language of its track.
  • Sliders: arrow keys, Home, End, Page Up and Page Down. An arrow key moves the seek slider one second, and the volume slider one step (5 % in the example). The seek slider says the time as text.
  • When the player itself has focus, not one of its controls (after a click on the video, for example): Space or K plays and pauses, and M mutes. F goes full screen and C turns captions on or off when the player has those buttons. With Ctrl, Alt or Cmd held, these keys do nothing.
  • The player is a role="group" named by its aria-label.

Contrast and focus are in the Design tab.

Design

When to use

Use a player to play a video, a sound or a MIDI file inside the page, when the reader starts it.

When not to use

Do not use a player for media that starts on its own or plays in the background.

  • Never autoplay.
  • For a short animated illustration with no sound, use an image or a figure.

Do and don't

Do Controls the reader starts, and a name that says what plays. Shortened: the full markup is under Copy the markup.
<div class="luz-player" data-type="audio" role="group" aria-label="Episode 12 player">
  <audio class="luz-player-audio" preload="metadata" controls src="episode-12.mp3"></audio>
</div>
Don't Media that starts on its own. Sound that plays for more than 3 seconds must have a way to stop it (WCAG 1.4.2), so it is shown as code only.
<video autoplay loop src="intro.mp4"></video>

Writing the content

Name each player by what it plays: "Episode 12 player", not "Audio player".

  • Give video captions (<track kind="captions">) and a transcript when it has speech. Captions include the sounds as well as the words; subtitles (kind="subtitles") translate the words.
  • Label each track in its own language: "English", "Español".
  • Describe a silent video in its aria-label or next to it.
  • Say the length and the format next to the player when it is large.

Contrast and focus

The controls sit on the strong surface. The seek bar's thumb and played part are the brand colour, and the bar has an edge in the field border colour, so its whole length shows.

PartColour on backgroundLightDarkHigh contrastNeeds
Time and status
--luz-text-secondary on --luz-surface-strong
5.3:1
5.4:1
16.5:1
4.5:1
Button icon
--luz-text-primary on --luz-surface-strong
13.5:1
10.9:1
18.0:1
3:1
Button icon, hover
--luz-brand-primary on --luz-surface-hover
5.1:1
6.4:1
13.5:1
3:1
Captions button when on: icon and underline
--luz-brand-primary on --luz-surface-strong
4.8:1
5.6:1
12.1:1
3:1
Seek bar thumb
--luz-brand-primary on --luz-surface-strong
4.8:1
5.6:1
12.1:1
3:1
Played part against the loaded part
--luz-brand-primary on --luz-surface-pressed
4.6:1
5.6:1
12.1:1
3:1
Played part against the part not loaded yet
--luz-brand-primary on --luz-surface-muted
5.6:1
7.7:1
13.5:1
3:1
Edge of the seek bar
--luz-input-border on --luz-surface-strong
3.2:1
3.0:1
18.0:1
3:1
Caption text
--luz-text-on-fill on --luz-surface-media
21.0:1
21.0:1
21.0:1
4.5:1
Focus ring
--luz-focus-ring on --luz-surface-strong
4.8:1
5.6:1
18.0:1
3:1

Measured from the token values of each mode, rounded down. Needs is the WCAG 2.2 AA minimum: 4.5:1 for text, 3:1 for focus rings, borders and icons.

  • Focus: buttons, sliders and the track select get a 2 px --luz-focus-ring outline 2 px outside, as every LUZ control.
  • Not colour alone: play and pause swap their icon and their name; the seek bar's thumb shows the position; the captions button, when on, gets an underline as well as the brand colour.
  • Track select: the form field's colours, from Form.
  • Target: the buttons are 32 px square.

Tokens it uses

The 25 tokens that components/player.css reads, by family. Each family links to its page, with the values in every mode. In Figma they are variables with the same names: see Tokens in Figma.

Spacing and sizing
--luz-control-md, --luz-space-2xs, --luz-space-3xs, --luz-space-md, --luz-space-sm, --luz-space-xs
Typography
--luz-font-mono, --luz-text-sm, --luz-text-xs
Radius
--luz-radius-md, --luz-radius-sm
Shadows
--luz-shadow-icon
Motion
--luz-duration-fast

Keyboard and ARIA are in the Code tab.