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 carriesaria-label="Volume". Native range elements expose value viaaria-valuenowautomatically. - 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
| Token | Default | Description |
|---|---|---|
--brio-media-bg | var(--color-surface) | Background color for the host and controls bar |
--brio-media-border | var(--color-border-subtle) | Border color for the host container |
--brio-media-radius | var(--radius-md) | Corner radius for the host container |
--brio-media-controls-color | var(--color-text) | Icon and text color for transport controls |
--brio-media-controls-hover | color-mix(in oklch, currentColor 10%, transparent) | Hover background for transport buttons |
--brio-media-time-color | var(--color-text-muted) | Color for the time display |
--brio-media-track-h | 0.25rem | Height of the range track for timeline and volume |
--brio-media-track-bg | var(--color-fill) | Background color for the unfilled portion of range tracks |
--brio-media-track-fill | var(--color-primary) | Color for the filled portion of range tracks |
--brio-media-thumb-size | 0.875rem | Diameter of the range thumb |
--brio-media-thumb-bg | var(--color-primary) | Fill color for the range thumb |
Classes
| Class | Description |
|---|---|
brio-media | component root |
CSS Reference: Brio Media Controls
Transport controls bar for media playback
Source: src/css/extras/brio-media.css
Tokens
| Token | Default | Description |
|---|---|---|
--brio-media-overlay-scrim | oklch(0% 0 0 / 0.85) | Scrim opacity at the bottom of the overlay gradient |
--brio-media-overlay-fade | opacity var(--duration-normal) var(--ease-in-out) | Show and hide opacity transition for overlay controls |
--brio-media-overlay-play-size | 2.5rem | Diameter of the enlarged play button in overlay mode |
Slots
| Slot | Description |
|---|---|
.brio-media-time | Elapsed and total duration display |
.brio-media-timeline | Timeline scrubber range input |
.brio-media-volume | Volume slider range input |
Classes
| Class | Description |
|---|---|
.brio-media-controls | component root |