Audio and video

Native media element wrapper

Intent

<brio-media> wraps a native <audio> or <video> child and, on JS upgrade, replaces the browser's UA controls with brio's own. Same chrome on every browser.

The transport controls are media-specific atoms, not .button and .input -- a play / pause key, a scrub bar, and a volume slider communicate continuous playback state, not discrete-input commands, so they get their own visual rhetoric. Each one is themable through a small --brio-media-* token spine; the Customization section below enumerates them.

Native media controls aren't portably styleable. Firefox doesn't expose pseudo-elements for the audio/video shadow DOM at all; WebKit exposes a few but not consistently. Authored controls drawn against the HTMLMediaElement API are the only way to ship a uniform player. That's what this element does.

Bare elements

Without a <brio-media> wrapper, native <video> and <audio> elements render with the browser's UA controls. The reset layer styles them as block elements: video gets max-width: 100%, audio gets inline-size: 100% so it fills its container regardless of browser-specific intrinsic widths.

<video poster="/media/video-test.png" controls="" src="/brio/media/video-test.mp4"></video>
<audio controls="" src="/brio/media/audio-test.mp3"></audio>

Video

Wrap a <video> in <brio-media>. Keep the controls attribute on the video -- it's the no-JS fallback (see below). On JS upgrade, brio-media strips controls and inserts its bar at the bottom of the host: play/pause, timeline, time display, mute, volume, fullscreen.

<brio-media>
    <video poster="/media/video-test.png" controls="" src="/brio/media/video-test.mp4"></video>
</brio-media>
<brio-media overlay="">
    <video poster="/media/big-buck-bunny-1080p-5s.jpg" controls="" src="https://upload.wikimedia.org/wikipedia/commons/c/c0/Big_Buck_Bunny_4K.webm"></video>
</brio-media>

Audio

Same wrapper, audio child. The fullscreen button is omitted automatically (gated on the child being an HTMLVideoElement); the rest of the bar is identical.

<brio-media>
    <audio controls="" src="/brio/media/audio-test.mp3"></audio>
</brio-media>

Overlay style (video only)

Add the overlay attribute to a video host for an overlaid transport: controls float absolutely over the media with a gradient scrim, recolor light-on-dark for legibility against any video content (and any active theme), and the play button enlarges into a circular shape. Move the mouse, tap, or focus a control to reveal the bar; with no interaction for a few seconds while playback is in progress, the bar fades out. Pause and it stays visible.

Audio silently ignores overlay. There is no surface to fade against, and the inline transport already serves the same intent.

<brio-media overlay="">
    <video poster="/media/video-test.png" controls="" src="/brio/media/video-test.mp4"></video>
</brio-media>
<brio-media overlay="">
    <video poster="/media/big-buck-bunny-1080p-5s.jpg" controls="" src="https://upload.wikimedia.org/wikipedia/commons/c/c0/Big_Buck_Bunny_4K.webm"></video>
</brio-media>

Without JavaScript

The controls attribute on the inner media is the fallback. If JS fails to load or upgrade, the user gets the browser's UA controls inside brio's chromed box -- still functional, still themable enough at the host level.

On upgrade, brio-media removes controls and builds its bar. The transition happens on connectedCallback, which fires once the element is parsed and inserted into the DOM, so the UA controls are visible only briefly during the upgrade window (typically one frame) on cold loads.

Behavior

All UI state derives from the HTMLMediaElement interface. brio-media listens for play, pause, timeupdate, loadedmetadata, durationchange, ended, and volumechange on the media, and for fullscreenchange on the document. Buttons drive media.play() / media.pause(); ranges drive media.currentTime and media.volume.

requestFullscreen() targets the host <brio-media>, not the inner <video>, so the controls bar stays visible in fullscreen mode. The host gets a black backdrop and object-fit: contain on the video so aspect ratio is preserved.

Listener cleanup uses an AbortController: the component creates one signal in connectedCallback and aborts it in disconnectedCallback, which removes every listener (media events, button clicks, range input, document fullscreenchange) in a single call.

On video with overlay, brio-media adds an idle-hide layer: it listens for pointermove, pointerdown, pointerleave, focusin, and focusout on the host plus the media's play / pause / ended, and toggles the idle attribute on the host so CSS can fade the bar. Audio silently ignores the variant.

Opt-out attributes

Optional controls can be suppressed via bare attributes on the host. Play / pause and the timeline are always present.

no-volume -- suppress mute button and volume slider

<brio-media no-volume="">
    <audio controls="" src="/brio/media/audio-test.mp3"></audio>
</brio-media>

no-fullscreen -- suppress fullscreen button (video only)

<brio-media overlay="" no-fullscreen="" no-volume="">
    <video poster="/media/video-test.png" controls="" src="/brio/media/video-test.mp4"></video>
</brio-media>

Reference

Selector Source Purpose
brio-media Author markup Host element. Wraps a single <audio> or <video> child.
.brio-media-controls JS-built The controls bar. Inserted as a sibling of the media on upgrade; hosts the per-control atoms below.
.brio-media-play JS-built Play / pause toggle button. Promoted to a circular shape in the overlay variant.
.brio-media-mute JS-built Mute / unmute toggle button.
.brio-media-fullscreen JS-built Enter / exit fullscreen button (video only).
.brio-media-timeline JS-built The seek range. Scrubs currentTime; disabled when duration is unknown (live streams).
.brio-media-volume JS-built The volume range. Drives media.volume; dragging up while muted unmutes.
.brio-media-time JS-built The current / total time display. Monospace with tabular figures so digits don't shift width as time advances.
overlay Author markup Bare attribute on the host. Video only. Floats the controls over the media with a gradient scrim, recolors light-on-dark, enlarges the play button into a circle, and adds idle auto-hide. Audio silently ignores it.
idle JS-applied Bare attribute on the host. Added on overlay video while playback is in progress and pointer / focus have been idle for a few seconds. CSS fades the bar; removed on any activity.
no-volume Author markup Bare attribute on the host. Suppresses the mute button and volume slider.
no-fullscreen Author markup Bare attribute on the host. Suppresses the fullscreen button (video only; no effect on audio).

Customization

Every theming hook lives on the brio-media host. Box chrome (--brio-media-bg, --brio-media-border, --brio-media-radius) controls the outer surface. A second group covers the transport atoms: --brio-media-controls-color for icon and time text, --brio-media-controls-hover for the subtle button hover background, and a small range spine (--brio-media-track-h, --brio-media-track-bg, --brio-media-track-fill, --brio-media-thumb-size, --brio-media-thumb-bg) for the timeline and volume slider. The track-fill token is the elapsed / value portion of the range -- recolor it to brand the most prominent piece of player chrome.

The overlay variant rescopes the controls tokens to a fixed light-on-dark palette inside brio-media[overlay] so the controls stay legible against any video content regardless of theme. Three overlay-only tokens (--brio-media-overlay-scrim, --brio-media-overlay-fade, --brio-media-overlay-play-size) cover the gradient bottom stop, the show / hide transition, and the enlarged play button diameter.

Full token list and defaults: src/extras/css/brio-media.css (file header).

Accessibility

  • Buttons carry stateful aria-labels. The play button toggles between "Play" and "Pause"; mute toggles "Mute" / "Unmute"; fullscreen toggles "Enter fullscreen" / "Exit fullscreen". Screen readers announce the current state when focus enters the button.
  • Ranges have semantic labels. The timeline carries aria-label="Seek" and the volume range carries aria-label="Volume". Native range elements expose value via aria-valuenow automatically.
  • Time display is not a live region. Updating every 250ms while playing would spam screen readers. Users who want elapsed time can poll the timeline range value via standard range AT support.
  • Captions for video are author-responsibility. Add <track kind="captions" srclang="..." src="..."> children of the <video>; WCAG 2.1 SC 1.2.2 requires captions for prerecorded video. brio-media doesn't render a captions toggle button in MVP; native UA caption rendering still works when tracks are present.
  • Transcripts for audio. Audio-only content needs a text alternative under WCAG 2.1 SC 1.2.1 -- place the transcript near the player or link to it from a clearly labeled control adjacent to the <brio-media>.
  • Don't autoplay with sound. Browsers block it, screen reader users get disoriented, and WCAG 2.1 SC 1.4.2 requires user-controllable audio that plays for more than three seconds.
  • Keyboard support comes from the focused primitive. Space toggles the focused button (play, mute, fullscreen). Arrow keys scrub or adjust volume on focused ranges. Tab moves between controls. No custom hotkey layer added on top.

CSS Reference: Brio Media

Audio and video media player host

Source: src/css/extras/brio-media.css

Tokens

TokenDefaultDescription
--brio-media-bgvar(--color-surface)Background color for the host and controls bar
--brio-media-bordervar(--color-border-subtle)Border color for the host container
--brio-media-radiusvar(--radius-md)Corner radius for the host container
--brio-media-controls-colorvar(--color-text)Icon and text color for transport controls
--brio-media-controls-hovercolor-mix(in oklch, currentColor 10%, transparent)Hover background for transport buttons
--brio-media-time-colorvar(--color-text-muted)Color for the time display
--brio-media-track-h0.25remHeight of the range track for timeline and volume
--brio-media-track-bgvar(--color-fill)Background color for the unfilled portion of range tracks
--brio-media-track-fillvar(--color-primary)Color for the filled portion of range tracks
--brio-media-thumb-size0.875remDiameter of the range thumb
--brio-media-thumb-bgvar(--color-primary)Fill color for the range thumb

Classes

ClassDescription
brio-mediacomponent root

CSS Reference: Brio Media Controls

Transport controls bar for media playback

Source: src/css/extras/brio-media.css

Tokens

TokenDefaultDescription
--brio-media-overlay-scrimoklch(0% 0 0 / 0.85)Scrim opacity at the bottom of the overlay gradient
--brio-media-overlay-fadeopacity var(--duration-normal) var(--ease-in-out)Show and hide opacity transition for overlay controls
--brio-media-overlay-play-size2.5remDiameter of the enlarged play button in overlay mode

Slots

SlotDescription
.brio-media-timeElapsed and total duration display
.brio-media-timelineTimeline scrubber range input
.brio-media-volumeVolume slider range input

Classes

ClassDescription
.brio-media-controlscomponent root