Design System

Design System

Video player

Media

Open in Storybook: Video player (opens in a new tab)

On this page

Overview

The video player is built on the native video tag, or audio for a sound with no picture, and on native form controls: buttons, two ranges and a select. The controls attribute is not used. Playback is driven through a ref, so every control can be named, styled and tested.

The captions are drawn outside the player, as ordinary text, so they zoom and wrap like any text and never cover the controls. The track is kept hidden: the browser loads it, fires its cues and draws nothing.

One DOM, three bars. The bar answers to the room the player has, in a container query: over the picture with every control, over the picture with fewer, or below the picture and wrapped. The thumbnail of a card is VideoThumbnail, a component of its own, because a card chooses it and the width of a page does not.

Informational video with sound never autoplays. autoplay, loop and muted belong only to decorative video and to clips with no sound.

Anatomy

  1. Picture

    video, audio with no controls

    Driven by a ref. Holds the source and the tracks. A click on it plays and pauses; it is not a tab stop, because the controls are always shown.

  2. Play/Pause

    button aria-label="Play video"

    The name changes with the state: “Pause video”. The icon is aria-hidden.

  3. Mute

    button aria-label="Mute"

    Becomes “Unmute” at 0. Unmuting brings the last volume back, or full volume if it was 0.

  4. Volume

    input type="range" min="0" max="10" aria-label="Volume"

    Read as a percent through aria-valuetext: “70% volume”. Arrow keys move it 10%.

  5. Current time

    time aria-hidden="true"

    The visible clock. Hidden so a parked screen-reader cursor does not read it again.

  6. Seek

    input type="range" aria-label="Seek"

    Says “13 seconds of 52 seconds” through aria-valuetext. Its value does not follow the video while a keyboard has it.

  7. Remaining time

    time aria-hidden="true"

    Visual only. The slider already says the position and the length.

  8. Captions toggle

    button aria-pressed aria-label="Captions"

    Name “Captions”. aria-pressed carries on and off, so the name never changes.

  9. Caption language

    select aria-label="Caption language"

    One option for each track, named in its own language. Shown for more than one.

  10. Full screen

    button aria-label="Enter full screen"

    Asks the whole player, so the bar and the captions stay on screen. “Exit full screen” when on.

  11. Captions

    section with a p

    The active cue as ordinary text, outside the player. The track mode is hidden.

Usage

Use cases

Functional elements where you can play, pause, seek, mute and open each design. Every video is cut from Sintel, an open movie.

  • Audio-only content

    Podcast or audio guide without picture: same controls, no captions toggle. A transcript is required (1.2.1) and its button is right below the player.

    Sintel · The trailer, sound only

    audio with no controls, the same div role="group", a transcript button

  • Decorative hero, 5 s or more

    Muted, aria-hidden, not focusable. Autoplay allowed. A visible Pause control is required.

    Open movie

    Meet Sintel and her dragon

    video muted autoplay loop aria-hidden="true" tabindex="-1", button “Pause background video”

  • Decorative hero · reduced motion

    prefers-reduced-motion: the video is replaced by a static image. No Pause needed.

    Open movie

    Meet Sintel and her dragon

    img alt="", no video, no button

  • Decorative micro-loop, under 5 s

    Muted and hidden from AT. Pause not required. Hidden under reduced motion.

    A dragon in flight

    Small loop next to the title. No controls: it lasts under 5 seconds and is hidden from assistive tech.

    video muted autoplay loop aria-hidden="true", no controls

  • Informational, dialogue + audio, 5 s or more

    No autoplay: the user starts it. Captions and a recommended transcript.

    track kind="captions", button aria-pressed, select for the language, transcript button

  • Informational, dialogue + audio, under 5 s

    Same rules even if short: no autoplay with sound, captions required.

    I'm searching for someone.

    track kind="captions", no autoplay

  • Meaningful sound, no dialogue

    Manual play. Captions describe the sounds; never label them “subtitles”.

    [music]

    track kind="captions" with “[music]”, no autoplay

  • Silent but meaningful, 5 s or more

    No sound, so no mute or captions. Transcript or audio description required, a button below.

    track kind="descriptions", button aria-pressed “Audio description”, transcript button

  • Silent but meaningful, under 5 s

    Muted autoplay allowed; Pause optional. Short description below; hidden under reduced motion.

    A young woman reaches out her hand as a small dragon flies past.

    video muted autoplay, a p that says what it shows

  • Complex visuals + audio

    No autoplay. Captions plus an audio description track (or extended transcript).

    What brings you to the land of the gatekeepers?

    track kind="captions" and track kind="descriptions", button aria-pressed “Audio description”

  • Thumbnails in cards

    The Play button is its own control, not nested in the card link. Its accessible name carries the title and duration. Play opens the full player in a modal dialog, and the guide opens in a drawer.

    • Sintel, the trailer

    • Sintel, searching for someone

    • Sintel, a dragon in the sky

    button aria-haspopup="dialog" aria-label="Play video: Sintel, the trailer, 52 seconds"

Props

Props of VideoPlayer
PropDoesDefault
src · labelThe file, and the name of the player as a wholeRequired
mediavideo, or audio for a sound with no picture: a dark panel, a title, no captions, no full screenvideo
titleThe title above the bar of an audio player—
languagesOne track kind="captions" each: code, label, src. None means no Captions buttonNone
languageSelectThe language select, forced on or offMore than one language
descriptionsA track kind="descriptions". Adds the Audio description button, and each new description is shown as text and said with ariaNotify()None
silentA video with no sound: no Mute and no Volumefalse
transcriptThe button below the player that opens the text in a drawer: label, title, text and side, left or rightNone
noteWords under a silent video that say what it shows—
startAt · captionsOn · audioDescriptionOnWhere it starts in seconds, and the choices a person has already made0 · false · false
autoPlayMuted autoplay, for a clip with no sound onlyfalse
The family
ComponentWhat it isProps
VideoControlButtonThe 44 × 44 button of the bar, with its iconicon, label, pressed
VideoThumbnailThe picture of a video in a card: one Play button that opens the player in a modal dialogtitle, duration, poster, captions, player
VideoTextDrawerA button that opens a text in a drawer, from the left or the right: the transcript below a player, the guide of a cardlabel, title, text, side, icon
DecorativeVideoA background loop: muted, hidden, with Pause when it moves for 5 seconds or moresrc, poster, pausable, whenReduced, reducedMotion

Example

The player is as wide as its slot and answers to it. Give the slot the width, never the player.

import { VideoPlayer } from '@/components/VideoPlayer'

<VideoPlayer
  src="/sintel-trailer.mp4"
  label="Sintel, official trailer"
  languages={[
    { code: 'en', label: 'English', src: '/trailer-en.vtt' },
    { code: 'it', label: 'Italiano', src: '/trailer-it.vtt' },
  ]}
  transcript={{
    label: 'Read the transcript',
    title: 'Transcript: Sintel, official trailer',
    text: transcript,
  }}
/>

Specs

States

States
StatePictureBar
Paused, before playbackThe poster circle. No autoplay for video with soundPlay/Pause shows Play
PlayingThe videoPause shows. The clock and the fill follow it
Captions onThe cue is text below the pictureCaptions is pressed: a lighter fill and a bar under the icon
Focus on a control—A 3px white ring. The seek slider has the ring as a border
Muted—The button reads Unmute and the volume slider is at 0
Audio onlyNone: a dark panel with the titleThe same bar, without Captions or Full screen
Text at 200%, 400% zoomUnchangedBelow the picture and wrapped. Every control stays at 44px or more

Tokens

Component tokens

Component tokens
PartPropertyTokenValue
Bar over the pictureFillink#16161a at 88%
Bar below the picture, cue, selectFillink#16161a
Poster, thumbnail badgesFillink#16161a at 85% and 90%
PictureFill until it drawsfill#f3f3f5 to #2c2c33
Control buttonHover · pressed—White at 16% · white at 24%
Controls, times, cueText and icons—#ffffff, on a dark surface
FocusRing—3px #ffffff. It replaces the blue site ring inside the bar
TrackFill—White at 35%, and white for the part played
Transcript linkTextlilac-deep#3d3459
Note, descriptionTextink-soft#4b4b55
CardBorder · fillline · card#d7d7dc · #ffffff

Guidelines

Controls by scenario and context

Each row is one scenario. Each column shows the control bar that design gets. Bars are scaled-down copies of the real controls (the control button is the component itself). Desktop uses the Large bar, smaller viewports the Medium bar or the thumbnail, and zoom the reflow layout.

  • Solid: required in that context, never dropped
  • Dashed: optional, or only if there is room (otherwise in the expanded player)
  • Transcript linkOutside the player, below it

Zoom 400% · 320 CSS px. At 200% text size the desktop bar stays and wraps.

  • Decorative hero, 5 s or more

    Background video in the header

    Live example

    Reduced motion

    Desktop · 640 px and upSize=Large

    Pause

    Pause only. Video is muted and aria-hidden. Name: “Pause background video”.

    Smaller viewport · 400–639 pxSize=Medium · in cards: Thumbnail

    Pause

    Same Pause button. Never dropped: 2.2.2.

    Zoom 400% · 320 CSS pxReflow layout

    Pause

    Same button, 44×44 at 1×. After reflow it must not cover the heading. Reduced motion: static image, no button.

  • Decorative micro-loop, under 5 s

    Small loop by a title

    Live example

    Desktop · 640 px and upSize=Large

    No controls

    Muted, aria-hidden, autoplay allowed. Under 5 s, so 2.2.2 needs no Pause.

    Smaller viewport · 400–639 pxSize=Medium · in cards: Thumbnail

    No controls

    Same.

    Zoom 400% · 320 CSS pxReflow layout

    No controls

    Same. Reduced motion: hidden or first frame only.

  • Informational, dialogue + audio, 5 s or more

    Tutorial or interview

    Live example

    Desktop · 640 px and upSize=Large

    Play/Pause · Seek · Time · Mute · Volume · Captions · Caption language · Full screen

    Transcript

    Every control. No autoplay. Captions outside the player. Transcript link below (recommended).

    Smaller viewport · 400–639 pxSize=Medium · in cards: Thumbnail

    Play/Pause · Seek (if room) · Time (if room) · Mute · Captions · Expand

    Card → Thumbnail

    Medium: Play, Mute, Captions, Expand always. Seek and time only if room. In a card under 400 px: Thumbnail, Play opens the dialog.

    Zoom 400% · 320 CSS pxReflow layout

    Play/Pause · Mute · Captions · Expand · Seek (if room) · Time (if room)

    320 CSS px: required buttons on row 1, seek on its own row. Volume and caption language go to full screen. Captions wrap below.

  • Informational, dialogue + audio, under 5 s

    Short tagline with voice-over

    Live example

    Desktop · 640 px and upSize=Large

    Play/Pause · Seek · Time · Mute · Volume · Captions · Caption language · Full screen

    Identical to the 5 s+ row. Being short removes nothing: captions still required.

    Smaller viewport · 400–639 pxSize=Medium · in cards: Thumbnail

    Play/Pause · Seek (if room) · Time (if room) · Mute · Captions · Expand

    Identical to the 5 s+ row.

    Zoom 400% · 320 CSS pxReflow layout

    Play/Pause · Mute · Captions · Expand · Seek (if room) · Time (if room)

    Identical to the 5 s+ row.

  • Informational, meaningful sound, no dialogue

    Coffee machine hissing demo

    Live example

    Desktop · 640 px and upSize=Large

    Play/Pause · Seek · Time · Mute · Volume · Captions · Full screen

    Captions describe sounds: [steam hissing]. Caption language only if the sound captions are translated.

    Smaller viewport · 400–639 pxSize=Medium · in cards: Thumbnail

    Play/Pause · Seek (if room) · Time (if room) · Mute · Captions · Expand

    Play, Mute, Captions, Expand always.

    Zoom 400% · 320 CSS pxReflow layout

    Play/Pause · Mute · Captions · Expand · Seek (if room) · Time (if room)

    Same wrap as dialogue. Sound captions stay readable below.

  • Informational, silent but meaningful, 5 s or more

    Product shown visually, no sound

    Live example

    Desktop · 640 px and upSize=Large

    Play/Pause · Seek · Time · Audio description · Full screen

    Transcript

    No audio track, so no Mute, Volume or Captions. AD toggle if there is an AD track; transcript always.

    Smaller viewport · 400–639 pxSize=Medium · in cards: Thumbnail

    Play/Pause · Seek (if room) · Time (if room) · Audio description · Expand

    Transcript

    Play, AD, Expand always. Transcript stays below the player.

    Zoom 400% · 320 CSS pxReflow layout

    Play/Pause · Audio description · Expand · Seek (if room) · Time (if room)

    Transcript

    Required buttons on row 1. Transcript reflows below.

  • Informational, silent but meaningful, under 5 s

    Short animation showing a feature

    Live example

    Desktop · 640 px and upSize=Large

    Pause (optional)

    Description

    Muted autoplay allowed; Pause optional. Short text description next to it.

    Smaller viewport · 400–639 pxSize=Medium · in cards: Thumbnail

    Pause (optional)

    Description

    Same. If Pause is shown it keeps 44×44.

    Zoom 400% · 320 CSS pxReflow layout

    Pause (optional)

    Description

    Same. Reduced motion: static frame plus the description.

  • Informational, complex visuals + audio

    Product video with dense visuals and narration

    Live example

    Desktop · 640 px and upSize=Large

    Play/Pause · Seek · Time · Mute · Volume · Captions · Caption language · Audio description · Full screen

    Extended transcript

    Everything in the dialogue row plus the AD toggle (or an extended transcript).

    Smaller viewport · 400–639 pxSize=Medium · in cards: Thumbnail

    Play/Pause · Seek (if room) · Time (if room) · Mute · Captions · Audio description · Expand

    Play, Mute, Captions, AD, Expand always. Seek and time only if room.

    Zoom 400% · 320 CSS pxReflow layout

    Play/Pause · Mute · Captions · Audio description · Expand · Seek (if room) · Time (if room)

    Five required buttons on row 1, seek on row 2.

The component differs from the matrix in two places: from 336px, not 400px, its Medium bar is over the picture, so the player of a card or a phone keeps its controls with the video; and at 320 CSS px it wraps Volume and Caption language into the bar, as the reflow design draws it, instead of moving them to full screen.

Content

Content
PartName or textValue text
Play/Pause“Play video” · “Pause video”—
Mute“Mute” · “Unmute”—
Volume“Volume”“70% volume”
Seek“Seek”“13 seconds of 52 seconds”. Units are spelled out: “1:05” and “1:50” sound alike
Captions · language · description“Captions” · “Caption language” · “Audio description”—
Full screen“Enter full screen” · “Exit full screen”—
Thumbnail Play“Play video: Sintel, the trailer, 52 seconds”. The title and the duration—
Close“Close video” on the dialog of a thumbnail · “Close” on a drawer—
Decorative Pause“Pause background video”, with the visible text “Pause background”—
Transcript, guide“Read the transcript” · “Extended transcript with visual descriptions” · “Read the guide”. The drawer is headed “Transcript: Sintel, official trailer”—

Behavior

Behavior
EventResult
Play is pressedIt plays and the name becomes “Pause video”. Focus stays on the button
The picture is clickedIt plays or pauses. It is a pointer shortcut: the button is the keyboard way
Volume is taken to 0It is muted, and the button reads Unmute
UnmuteThe last volume comes back, or full volume if the slider was at 0
An arrow on SeekOne second, either way. The value is held while a keyboard has the slider and the video plays
Captions onThe cue is text below the picture. Changing the language changes the cue and keeps the toggle
Audio description onEach new description is shown as text and said once with ariaNotify() from the player
The transcript or the guide is pressedA modal drawer opens from the left or the right, with focus on its heading. Esc, Close or a click outside shuts it and focus goes back to the button
Play on a thumbnailA modal dialog opens with the Large player, focus on Play/Pause, and the video starts
Esc or Close on that dialogIt closes, the video pauses and keeps its place, and focus goes back to Play
Reduced motionA decorative hero is a still image with no button. A micro-loop is not there

Accessibility

Keyboard

Every control is native, so no key is handled by script except the seek arrows, which keep a fine step for a smooth thumb and jump a whole second. Seeking by keyboard means no dragging is needed.

Keyboard
KeyActionNative
Tab, Shift+TabPlay/Pause, Mute, Volume, Seek, Captions, Caption language, Audio description, Full screen, in that order. Hidden controls are not stops. The picture is not a stopYes
Enter, SpaceActivates the focused button. A toggle keeps its focusYes
Left, Right, Up, DownSeek: one second. Volume: 10%. Caption language: the next optionSeek no · Volume yes · Select yes
Home, EndVolume: 0% and 100%Yes
EscCloses the dialog of a thumbnail or the drawer of a transcript and returns focus to its button. Leaves full screen: the browser does itYes

Roles and properties

Roles and properties
PartExposedNote
PlayerA figure named by aria-label“Sintel, official trailer”
Controlsgroup named “Media controls”A div. A ul role="group" with li fails the axe rule listitem, so there is no list
Buttonsbutton, named by aria-labelThe icon is aria-hidden and focusable="false"
Captions, Audio descriptionaria-pressedThe name stays the same: the state is the property
Seek, Volumeslider with aria-valuetextNo aria-valuenow: the native value is read
TimesNothingaria-hidden. The sliders say the position and the length
Transcript, guideA button with aria-haspopup="dialog" that opens a modal dialogNamed by its heading, which takes focus. It is the system drawer, from either side
Cue, descriptionTextNot live regions. The description is also said with ariaNotify()

The silent seek slider

Two values do two jobs. The clock and the fill follow the video on every frame. The value of the slider is what a screen reader reads, and it must not follow the video, or the position would be read all the time. While a keyboard has the slider and the video plays, its value is held; it changes on focus and on a seek.

The silent seek slider
MomentSlider valueSaid
The slider gets keyboard focusSnapshot of the current time“13 seconds of 52 seconds”, once
The video plays onHeldNothing. The clock and the fill move
An arrow is pressedThe new timeThe new position, once
Mouse or touch focusFollows the videoNothing is said by the slider

Zoom and text size

Zoom and text size
CriterionWhat the player does
Text at 200%Size, controls, times and captions are in rem, so they grow with the text. The bar moves below the picture and wraps, and the seek bar gets its own row. Nothing is truncated, overlaps or is hidden
Reflow at 400%1280px at 400% is 320 CSS px. One column: picture, controls, captions. No horizontal scroll. Every control stays available and 44 × 44px or more, and none moves into a menu
Text spacingNo fixed height on the bar or the cue, so labels, times and captions do not clip
Captions while zoomedThey are outside the player, so they wrap freely and never cover the controls

Announcements

A native control says its own name, role and value, so most of the player needs no announcement. What changes without a control being touched is said once with ariaNotify() from the player, never through a live region. The method puts nothing in the DOM: where a browser does not have it nothing is said, and the same words are on the page as text.

Announcements
EventSaid byAnnouncedPriority
Volume changesThe slider, natively“80% volume”—
MuteThe button, by its name“Unmute”—
Focus on SeekThe slider, natively“13 seconds of 52 seconds”, once—
The video playsNothingSilence. The clock and the fill move, the value of the slider is held—
A new audio descriptionariaNotify() from the playerThe text of the cue, also on the pagenormal
A caption cueNothingIt is text on the page, outside the player. A screen reader reads it when asked—

The rules of ariaNotify(), as the system uses it.

  • One call for each event, after the DOM and the focus have changed.
  • Called on the player: an element that is in the accessibility tree and stays.
  • Priority normal. high is for what interrupts, like an error.
  • The voice follows the lang of the element it is called on.
  • Feature-detected: where the method is missing nothing is said, and no live region takes its place.
  • Never role="status", role="alert" or aria-live.
if ('ariaNotify' in Element.prototype) {
  player.ariaNotify(cue.text, { priority: 'normal' })
}

WCAG criteria

Every criterion the player answers to, once, with its level and where the player meets it. The other sections say what the player does, not which criterion it is.

WCAG criteria
CriterionLevelRequirement here
1.2.1 Audio-only and Video-only (Prerecorded)AAudio only has a transcript: a button below the player opens it
1.2.2 Captions (Prerecorded)AA Captions toggle and a track, outside the player, with the sounds that matter
1.2.3 Audio Description or Media Alternative (Prerecorded)AAn Audio description button and track, or a transcript
1.2.5 Audio Description (Prerecorded)AAAn audio description track for a video whose visuals carry meaning
1.4.2 Audio ControlAPause and Mute, never dropped. A thumbnail never plays sound and a decorative video is muted
1.4.4 Resize TextAASizes in rem: the bar, the times and the captions grow with the text
1.4.10 ReflowAAOne column at 320 CSS px, with every control, and no horizontal scroll
1.4.11 Non-text ContrastAAWhite on ink for the icons, the ring and the track
1.4.12 Text SpacingAANo fixed height on the bar or the cue
1.4.13 Content on Hover or FocusAANothing appears on hover or focus: the bar is always shown
2.1.1 KeyboardAEvery control is native. The two sliders take the arrows
2.2.2 Pause, Stop, HideAPlay/Pause, and Pause on a decorative video of 5 s or more
2.4.3 Focus OrderAThe source order, in every layout. The dialog and the drawers return focus to their button
2.4.7 Focus VisibleAAA 3px white ring on every control of the bar
2.5.7 Dragging MovementsAASeeking and volume by the arrows, no dragging
2.5.8 Target Size (Minimum)AA44 × 44px for every control, at every size
3.3.2 Labels or InstructionsAA name on every control
4.1.2 Name, Role, ValueAThe state in aria-pressed and aria-valuetext
4.1.3 Status MessagesAAA new description is said with ariaNotify()
Techniques—G68 · G159 · G166 · G203
European Accessibility Act—Built on WCAG 2.2, it requires media content to be accessible; following these rules keeps the player compliant

Related

References

This section is still in progress.

Transcript: Sintel, the trailer, sound only

0:05 [music]

0:12 What brings you to the land of the gatekeepers?

0:18 I'm searching for someone.

0:21 [music]

0:36 A dangerous quest for unknown hunter.

0:41 I've been alone for as long as I can remember.

0:44 [music]

Transcript: Sintel, official trailer

0:05 [music]

0:12 What brings you to the land of the gatekeepers?

0:18 I'm searching for someone.

0:21 [music]

0:36 A dangerous quest for unknown hunter.

0:41 I've been alone for as long as I can remember.

0:44 [music]

Transcript: Sintel, a dragon in the sky, no sound

0:00 A large dragon crosses a golden sky, and a smaller one flies below it.

0:03 A young woman with short red hair watches a small dragon fly past.

0:05 Her face is lit warmly. She holds out her hand.

0:06 The small dragon flies past her outstretched, gloved hand.

Extended transcript: Sintel, official trailer

0:04 [Description] A hooded traveller climbs a snowy mountain in fog.

0:05 [music]

0:10 [Description] A title card: The Blender Foundation presents.

0:12 [Description] A bearded old man in a headband speaks.

0:12 What brings you to the land of the gatekeepers?

0:18 [Description] A figure walks down a narrow, dark street between wooden houses.

0:18 I'm searching for someone.

0:21 [music]

0:24 [Description] Small dragons fly over a ruined city at sunset. A larger one crosses the sky.

0:29 [Description] A young woman with short red hair watches a small dragon fly past.

0:33 [Description] The small dragon flies past her outstretched hand.

0:36 A dangerous quest for unknown hunter.

0:37 [Description] A lone figure crosses desert dunes.

0:41 I've been alone for as long as I can remember.

0:43 [Description] The Sintel emblem: a tree inside a circle.

0:44 [music]

0:48 [Description] Credits on a black screen: The Durian Open Movie Project.

Guide: Sintel, the trailer

Sintel is an open movie made by the Blender Foundation.

The trailer crosses three worlds: the snow, the city and the desert.

It is free to share under a Creative Commons licence.

Guide: Sintel, searching for someone

A short clip with one spoken line.

It has its own captions, in English and Italian.

Play it, then turn the captions on.

Guide: Sintel, a dragon in the sky

Eight seconds of music and no dialogue.

The captions say what is heard: the music.

The same clip with no sound has an audio description instead.