Skip to content

Video

<loomi-video> is a themeable wrapper around the native <video> element. The browser’s real media element does all the codec/format/network work under the hood; on top of it, loomi-video layers a themeable, keyboard-accessible control bar built from @loomidev/button and @loomidev/slider — instead of the browser’s native control UI.

Terminal window
npm install @loomidev/video lit
import "@loomidev/video";
<loomi-video src="/demo.mp4" controls poster="/poster.jpg"></loomi-video>

Without controls, loomi-video is a bare, unstyled passthrough — exactly like a plain <video> with no controls attribute. Set controls to opt into the loading state, error state, click-to-play overlay, and themed control bar (play/pause, seek, volume, captions, picture-in-picture, fullscreen).

<source> and <track> children work exactly like plain HTML — they’re moved onto the real internal <video> element on connect (Shadow DOM slotting alone can’t make a native media element discover them itself, so loomi-video does this move for you).

<loomi-video controls poster="/poster.jpg">
<source src="/demo.webm" type="video/webm" />
<source src="/demo.mp4" type="video/mp4" />
<track kind="subtitles" src="/captions-en.vtt" srclang="en" label="English" default />
<track kind="subtitles" src="/captions-fr.vtt" srclang="fr" label="Français" />
</loomi-video>

When one or more <track> children are present, a captions button appears in the control bar. Clicking it opens a small menu listing “Off” plus each track’s label; press “c” while the player is focused to cycle captions on/off without opening the menu.

color picks the accent used for the seek/volume sliders, the play button, and focus rings — any loomi color.

<loomi-video src="/demo.mp4" controls color="success"></loomi-video>
<loomi-video src="/demo.mp4" controls color="error"></loomi-video>
<loomi-video src="/demo.mp4" controls aspect-ratio="1 / 1" fit="cover"></loomi-video>

aspect-ratio reserves the right amount of space before metadata loads (no layout shift). fit maps to object-fitcontain (default), cover, or fill.

Replace the entire built-in control bar with your own markup via the controls slot — loading, error, and click-to-play overlays are unaffected.

<loomi-video src="/demo.mp4" controls>
<div slot="controls">
<button onclick="this.closest('loomi-video').togglePlay()">Play / Pause</button>
</div>
</loomi-video>

While the browser is buffering (initial load or a mid-playback stall), loomi-video shows a themed @loomidev/spinner overlay. If the media fails to load — a bad URL, an unsupported format, a network error — it shows a friendly message with a “Retry” button that reloads the source. Listen for the loomi-video-error event to hook in your own logging/telemetry.

With the player focused (click it, or Tab to it):

KeyAction
Space / kToggle play/pause
/ Seek 5s back/forward
/ Volume up/down
mToggle mute
fToggle fullscreen
cToggle captions (if a track is present)
Home / EndSeek to start/end

Every control in the bar (buttons, sliders) is independently focusable and reachable by Tab, in addition to these shortcuts.

Both use the standard browser APIs (requestFullscreen/exitFullscreen, requestPictureInPicture/exitPictureInPicture) on the player itself, so your control bar stays visible in both modes. The picture-in-picture button is hidden automatically when the browser doesn’t support it (or set disable-pip to always hide it); set disable-fullscreen to hide the fullscreen button.

loomi-video is built on semantic markup where the browser gives us the right behavior, and adds ARIA only where the component has custom interaction. Every icon-only control has an accessible label (aria-label or visually-hidden text), the error overlay uses role="alert", and the captions menu uses role="menu"/menuitemradio.

  • Supports keyboard focus with visible :focus-visible styling on the player and every interactive control.
  • Full keyboard shortcut set (see above) in addition to individually tabbable controls.

For the library-wide baseline, see Foundations — Accessibility.

The player fills its container width (width: 100%) and reserves height via aspect-ratio, so it behaves predictably in cards, grids, and flexible layouts. The control bar’s volume slider and time labels hide automatically in narrow containers (via CSS container queries) rather than overflowing or wrapping awkwardly.

For the shared container and viewport rules, see Foundations — Responsive behavior.

loomi-video’s control bar is designed to sit over video content regardless of your page’s theme — it uses a translucent dark scrim and white icons by design, not the light/dark semantic tokens. The accent color (play button, sliders, focus rings) still follows color and picks up .dark overrides from @loomidev/theme-switcher the same as every other loomi component.

For theme activation, token overrides, and contrast guidance, see Foundations — Dark mode.

AttributeDefaultDescription
src(blank)Video URL. Omit it and use <source> children instead for format fallback.
poster(blank)Poster image URL, shown before playback starts.
controlsfalseShows the themed control bar, loading/error states, and click-to-play overlay. (boolean)
autoplayfalse(boolean)
loopfalse(boolean)
mutedfalse(boolean)
preloadmetadatanone | metadata | auto.
playsinlinetrueDefaults to true (unlike native <video>) so iOS Safari doesn’t hijack playback into its own native fullscreen player, which would hide the control bar. Set playsinline="false" to opt back into that native behavior. (boolean)
crossorigin(blank)"" | anonymous | use-credentials.
colorprimaryAccent color for the control bar. Any loomi color.
aspect-ratio16 / 9Any valid CSS aspect-ratio value.
fitcontaincontain | cover | fill — maps to object-fit.
autohide-controlstrueHides the control bar after a few seconds of inactivity while playing. (boolean)
disable-pipfalseAlways hides the picture-in-picture button. (boolean)
disable-fullscreenfalseAlways hides the fullscreen button. (boolean)
SlotDescription
(default)<source>/<track> children, forwarded onto the internal <video>.
controlsReplaces the entire built-in control bar with custom markup.

loomi-video re-dispatches key media events from the host element (the real <video> lives behind the Shadow DOM boundary, so this is how consumers observe it without reaching into internals):

EventDetail
play / pause / ended(none)
timeupdate{ currentTime, duration }
volumechange{ volume, muted }
fullscreenchange{ fullscreen }
enterpictureinpicture / leavepictureinpicture(none)
loomi-video-error{ code, message }. Not named error — that type bubbling to window reads as an uncaught page error to test harnesses and error-tracking tools.
const player = document.querySelector("loomi-video");
player.play();
player.pause();
player.togglePlay();
player.seek(30); // seconds
player.toggleMute();
player.setVolume(0.5); // 0–1
player.toggleFullscreen();
player.togglePictureInPicture();
player.selectTrack(0); // index into player's text tracks, or -1 for "off"

Read-only getters mirror the underlying media element: paused, ended, currentTime, duration. currentTime is also settable (player.currentTime = 30), and forwards to seek().

  • @loomidev/core
  • @loomidev/button
  • @loomidev/icon
  • @loomidev/slider
  • @loomidev/spinner