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.
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.
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.
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.
<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.
<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>
import { initPlayers } from "@luz-ds/core/player";
initPlayers();
Variants and options
| Class or attribute | Effect |
|---|---|
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) |
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-playeron the page, or inside the element you pass it.luz.jsruns it. A player already mounted is left as it is, so running it again is safe.- On mount, the script removes the native
controlsand binds the LUZ ones: play and pause, time, seek and volume. They start from the media's own state: amutedvideo 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 itsmaxin 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.::cuegives the cues light text on the media surface. Nothing is stored. A control with no track to show is hidden. mountPlayer(root)andmountMidiPlayer(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 itsaria-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
<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>
<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-labelor 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.
| Part | Colour on background | Light | Dark | High contrast | Needs |
|---|---|---|---|---|---|
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-ringoutline 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.
- Color
--luz-brand-primary,--luz-divider,--luz-focus-ring,--luz-input-border,--luz-surface-hover,--luz-surface-media,--luz-surface-muted,--luz-surface-pressed,--luz-surface-strong,--luz-text-on-fill,--luz-text-primary,--luz-text-secondary- 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.