Accessibility

What the player does for keyboards, screen readers, and viewer preferences, and what's left to you.

The controls are native buttons and range inputs, and their names and values follow the player's state. This page covers what the player does by itself, the keys it answers to, and the parts only you can provide.

What you provide

  • A name for the player. Set aria-label, or give VideoPlayer a text title. Otherwise the player is called "Video player", which you can translate through the player string.
  • Captions. The player shows them, but the files are yours. Add them through the captions plugin, and consider the transcript plugin for a version anyone can read at their own pace.
  • Translated labels. The i18n plugin covers every built-in label, and also sets lang and dir on the player.
  • The page around it. A heading for the video, and a lang on the page.
  • Names for your own controls. A custom button needs an aria-label, and anything it changes out of sight needs an announcement.
tsx
<VideoPlayer src="/coast.mp4" aria-label="The long coast, a short film" />

Keyboard

Tab reaches every control, and hidden controls come back when they take focus. Tab never gets stuck: it moves through an open panel and then on out of the player.

Tab follows the order of the DOM. In the default look and the TikTok and Instagram themes, that's also the order on screen. Other themes rearrange their controls with CSS, so Tab can move in a different order from the one you see.

Player shortcuts

These come from the hotkeys plugin. They work while focus is in the player, and afterwards for the player last used, until the viewer clicks somewhere else on the page.

KeysActionNeeds
Space or KPlay or pause
←, →Seek 5 seconds
J, LSeek 10 seconds
↑, ↓Change the volume by 5%
MMute or unmute
Home, EndGo to the start or the end
0 to 9Go to that tenth of the video
FFullscreenfullscreen
PPicture in picturepip
CCaptions on or offcaptions
>, <Faster or slowerspeed
?Show the shortcutsshortcuts

Controls

WhereKeysAction
Timeline←, →, ↑, ↓Seek a step
TimelinePage Up, Page DownSeek a larger step
TimelineHome, EndGo to the start or the end
VolumeArrow keysChange the volume
Menu buttonEnter, Space, ↑, or ↓Open the menu
Menu↑, ↓, Home, EndMove between rows
Menu→Open a submenu, or ← in right-to-left languages
Menu←Go back a level, or → in right-to-left languages
MenuEscGo back a level, then close
MenuTabClose and move on
MenuAny letterJump to the row that starts with it
Episodes←, →, Home, EndMove between episodes, mirrored in right-to-left languages
PanelsEscClose
TranscriptEnter on a lineSeek to it
Transcript↑, ↓, Page Up, Page Down, Home, EndScroll, which stops following along until "Back to the current line"

Every other plugin's control is a button, pressed with Enter or Space: skip, up next, unmute, skip ad, cast, AirPlay, share, screenshot, loop, and the mini player's close. Double-clicking and double-tapping the video only repeat what these keys and buttons already do.

Where focus goes

  • After Play: focus moves from the big play button to the one in the controls, and they're free to hide, so you can watch. Any key brings them back. Every other control keeps them up for as long as it has focus.
  • At the end: focus moves to Replay.
  • Menus and panels: closing one returns focus to where it was when it opened.
  • Controls that go away: when a skip button, an up next card, an unmute chip, or an ad's controls disappear with focus inside them, focus moves to the play button rather than being lost.
  • Clicks: a pointer doesn't draw the focus ring, only the keyboard does.

Screen readers

The player is a named group. Each control's name follows its state, such as Play becoming Pause, and the timeline and volume report a readable value in the viewer's language, such as "0:12 of 1:30".

What's announced

These are said without moving focus, because a sighted viewer sees them change on screen.

What changesWhen it's said
BufferingOnce a stall has lasted 2.5 seconds
A shortcut's resultEach press, such as "+5s" or "Volume 40%"
A skip buttonWhen it appears
Up nextWhen the card appears, not with each second of its countdown
The next item in a playlistWhen it starts
The unmute chipWhen autoplay falls back to muted
An adWhen each one starts, and when it can be skipped
CastingWhen a session starts
An errorAt once, through role="alert"

Time isn't announced during playback. The live region is only there for changes like these.

Captions are hidden from screen readers, so that they don't talk over the video's own sound. The transcript is a named list that marks the current line with aria-current, for anyone who wants to read the words.

Announcing your own changes

When a part of yours changes something the viewer can see without moving focus, say so with player.announce. It speaks through the player's polite live region, and it's safe to repeat the same message.

tsx
import { usePlayer } from "react-video-kit";function BookmarkButton() {  const player = usePlayer();  return (    <button      aria-label="Bookmark this moment"      onClick={() => {        saveBookmark(player.clock.time());        player.announce("Bookmarked");      }}    />  );}

Viewer preferences

The skin follows the viewer's system settings, with no setup.

SettingWhat changes
prefers-reduced-motionControls, menus, panels, and tooltips fade instead of moving. Lists jump rather than scroll smoothly. The buffering spinner slows down, and ambient light updates once a second. The autoplay plugin doesn't start the video.
prefers-reduced-transparencySheets and controls are solid, with no blur.
prefers-contrast: moreScrims and text get stronger, and the focus ring is thicker.
forced-colors: activeSliders draw their tracks, fill, and thumbs in system colours, buttons get an outline, and the focus ring uses Highlight.

Nothing plays until the viewer asks, unless you add the autoplay plugin.

Visual details

  • Focus ring: two tones, a ring in --rv-focus around a dark band, so one of them shows on any picture. Change the ring's colour with the --rv-focus token.
  • Text and icons: these sit on a scrim and carry a shadow, so they hold up over a bright frame.
  • Target size: buttons take clicks across up to 44px where there's room, and neighbours share the space where there isn't. At 360px wide, the smallest controls in any theme are 24px across.
  • Captions: they move up when the controls show, and stay on top of anything they still reach, with clicks passing through them. Set their look with --rv-caption-size, --rv-caption-color, and --rv-caption-background. To offer viewers a choice of size, set the token from a control of your own.
  • Zoom and text spacing: menus and panels grow or scroll with larger text and wider spacing.

Testing your player

Most of what matters is quick to try by hand:

  1. Put the mouse aside, and use the whole player with the keyboard: start it, seek, open each menu and panel, and leave.
  2. Check that you can always see where focus is, on bright frames as well as dark ones.
  3. Turn on a screen reader, and listen to what each control says and what's announced as things change.
  4. Zoom to 200%, and try your system's contrast, motion, and transparency settings.

Automated tools can cover part of this, using axe with Playwright. Open your menus and panels first, because axe can only check what's on the page.

ts
import AxeBuilder from "@axe-core/playwright";import { expect, test } from "@playwright/test";test("the player has no detectable violations", async ({ page }) => {  await page.emulateMedia({ forcedColors: "active", reducedMotion: "reduce" });  await page.goto("/watch");  await page.locator('[data-part="root"]').hover();  await page.locator('[data-part="menu-trigger"]').click();  const { violations } = await new AxeBuilder({ page }).include('[data-part="root"]').analyze();  expect(violations).toEqual([]);});

Passing means axe found nothing it can detect, not that the player works well for everyone, so keep trying it by hand too.