Media
Loading playground
Overview
Media takes a Media value and renders it. The value is a discriminated union, so a single <Media :media> call site can receive an image, a video, or an audio asset. Media looks at media.type and dispatches:
- Images render with the built-in renderer, backed by
nuxt-image. It picks the right variant for the current viewport and handles<picture>source selection, customaspectRatio(boolean, string, or number), and breakpoint-awaresizesstrings. The bonuscmw(content-max-width) unit makessizesstrings inside constrained containers easier to write. - Video and audio render with built-in native players (
<video>/<audio>). Playback is set with props on<Media>. Register your own renderer to override the built-in for a type (for example a Vidstack wrapper for adaptive streaming). See Rendering video and audio.
The image prop surface is unchanged from when Media was image-only, so every existing <Media :media> call site keeps working. A call site that passes an image renders exactly as before; the same call site now also plays a video or audio asset, with no per-call-site change.
Reach for MediaPreview when you also want lightbox interaction, surface-tone awareness, and cross-image navigation. Use Media directly only when you do not want the lightbox shell.
Key Business & UX Benefits
- Backed by nuxt-image, so every storefront image ships in the right format and size for the device, cutting bandwidth costs and load times.
- A single discriminated
Mediavalue covers images, video, and audio, so connector and block code routes any asset through one component instead of hand-writing type branches. - Video and audio play out of the box with native players, and a one-line registration swaps in your own player for streaming or custom UI without forking the component.
- The
cmwunit makessizesstrings inside constrained containers easy to write, so the layout engine picks the smallest correct asset.
Feature List
- Backed by nuxt-image, so every storefront image ships in the right format and size for the current device
- Single typed `Media` value (discriminated union) renders images, video, and audio through one component
- Video and audio play out of the box with built-in native players; register your own renderer to override a type for streaming or custom UI
- A `playback` mode (`interactive` default, or `background` for a muted autoplay loop) sets video/audio behavior per placement; the individual `controls` / `autoplay` / `muted` / `loop` / `playsinline` / `disablePictureInPicture` props map 1:1 to native HTML and override the mode
- `playback="background"` runs a decorative video loop (autoplay suppressed under reduced motion); `v-model:paused` lets the consumer render and place its own WCAG-compliant pause control
- `aspectRatio` accepts boolean, string, or number, so callers pick between intrinsic, square, and named ratios from the same prop
- Breakpoint-aware `sizes` strings hint the browser to pick the smallest correct variant, cutting bandwidth on mobile
- Custom `cmw` (content-max-width) unit in `sizes` strings is honored inside constrained containers
- Handles `<picture>` source selection so AVIF, WebP, and JPEG fallbacks are emitted correctly
Sizing images with the sizes prop
Without a hint, the browser assumes an image fills the whole viewport and downloads the largest variant that could fit, even when the image occupies only part of the screen. The sizes prop tells the browser how wide the image actually renders at each breakpoint, so nuxt-image generates a matching srcset and the browser picks the smallest adequate file.
<template>
<Media :media="product.cover" sizes="100vw sm:50vw md:400px" />
</template>
Read that as: the image is 100vw wide by default, 50vw from the sm breakpoint up, and a fixed 400px from md up.
The format
sizes is a space-separated list of breakpoint:width pairs built on the laioutr-ui theme breakpoints (xs, s, sm, md, lg, xl, xxl):
- A value with no breakpoint prefix is the base size, applied from the smallest viewport up.
- Each prefixed pair applies from its breakpoint upward and overrides the previous one (mobile-first).
- Widths accept any unit a CSS
sizesattribute understands, such asvwandpx.
sizes="100vw md:50vw xl:600px" is 100vw below md, 50vw from md to just under xl, and 600px from xl up.
A single viewport-relative value is shorthand for that width on every breakpoint. sizes="50vw" becomes 50vw across all viewports.
The cmw unit
cmw (content-max-width) expresses a width as a percentage of the theme's content max-width at that breakpoint instead of the full viewport. Reach for it when the image sits inside a width-constrained container, such as the main content column, rather than bleeding to the screen edge.
<template>
<Media :media="article.hero" sizes="100vw md:50cmw" />
</template>
50cmw resolves to half of the content max-width configured for that breakpoint. Media looks up the breakpoint's content max-width in your theme and rewrites the cmw value to the equivalent px before handing the string to nuxt-image; with the default theme, md:50cmw becomes roughly 640px.
High-density screens
Media serves 1x-density images by default. Set retina to also emit 2x variants for high-DPI screens:
<template>
<Media :media="product.cover" sizes="md:400px" retina />
</template>
Providing a default sizes to a subtree
Instead of repeating sizes on every call site, wrap a subtree in MediaSizesProvider and set one default. Every <Media> rendered inside it inherits that value. The provider takes the sizes as a ref through its value prop:
<script setup lang="ts">
import { ref } from 'vue';
import { MediaSizesProvider } from '#ui-kit/components/Media/MediaSizesProvider';
const sizes = ref('100vw md:50cmw');
</script>
<template>
<MediaSizesProvider :value="sizes">
<slot />
</MediaSizesProvider>
</template>
A provided value overrides the sizes prop on the <Media> elements below it. So once MediaSizesProvider sets sizes, setting sizes on an individual descendant has no effect; change the provided value, or render that <Media> outside the provider.
Rendering video and audio
@laioutr-core/ui-kit<Media> plays video and audio out of the box with built-in native players: type: 'video' renders a native <video>, type: 'audio' a native <audio>. Pick a video in Studio and it plays, with no wiring. For adaptive streaming or a custom player UI, register your own renderer to override the built-in for that type.
Playback
Playback behavior is set with props on <Media>. The playback prop picks the intent; six individual props map 1:1 to the native HTML attributes and override whatever the mode implies.
playback | What it renders |
|---|---|
interactive (default) | A native player: controls on, nothing automatic. |
background | A decorative muted autoplay loop: autoplay, muted, loop, playsinline, and disablePictureInPicture on, controls off. Video only. |
Each attribute also has its own prop. An explicitly set prop always wins; an unset prop takes the mode default, so you reach for these only to deviate from the mode:
| Prop | interactive | background | Effect |
|---|---|---|---|
controls | true | false | Show the browser's native playback controls |
autoplay | false | true | Start playback automatically (the browser also requires muted) |
muted | false | true | Mute the media |
loop | false | true | Restart when playback ends |
playsinline | false | true | Play inline on iOS instead of going fullscreen (video only) |
disablePictureInPicture | false | true | Ask the browser to hide the Picture-in-Picture control (video only; a request, not a guarantee) |
These are deliberately not part of the Media value. A Media object describes the asset; whether a placement is a controllable player or a muted background loop is an editorial decision, so the calling component sets it. They apply to video (and to audio where meaningful); image media ignores them, and background is video-only. A controllable player needs no extra props:
<template>
<Media :media="episode.audio" />
</template>
autoplay only takes effect when the media is also muted; browsers block sound-on autoplay. background mode turns both on for you.
Background video
@laioutr-core/ui-kitplayback="background" turns a video into a decorative loop with one switch — autoplay, muted, loop, playsinline, and disablePictureInPicture on, controls off — instead of spelling out the whole cluster at every call site:
<template>
<Media :media="hero.video" playback="background" />
</template>
Individual props still override the mode, so a background loop that plays once becomes <Media :media="hero.video" playback="background" :loop="false" /> — the rest of the cluster stays on.
Reduced motion
A muted autoplay loop is decorative motion, so <Media> suppresses autoplay for visitors who set prefers-reduced-motion: reduce. The video does not start on its own; it settles on its poster frame, and the visitor can start it from the pause control you provide.
Pausing a background video
Auto-playing motion that runs longer than five seconds needs a pause mechanism (WCAG 2.2.2). <Media> ships no pause button of its own — where it sits is a layout decision — so it exposes the paused state through v-model:paused and leaves the control to you. Bind a ref and render your own button:
<script setup lang="ts">
import { ref } from 'vue';
// The consumer owns the paused state; <Media> seeds it on mount and keeps it in sync.
const paused = ref<boolean>();
const toggle = () => {
paused.value = !(paused.value ?? false);
};
</script>
<template>
<div style="position: relative">
<Media :media="hero.video" playback="background" v-model:paused="paused" />
<IconButton
style="position: absolute; inset-block-end: 1rem; inset-inline-end: 1rem"
:icon="paused ? 'media/play' : 'media/pause'"
:label="paused ? 'Play background video' : 'Pause background video'"
@click="toggle"
/>
</div>
</template>
<Media> seeds paused on mount from the reduced-motion-aware autoplay decision, so the button reflects reality on first paint — it shows "play" when autoplay was suppressed. It also reflects browser-initiated pauses (a data-saver mode, a backgrounded tab) back into the binding, so the icon and label stay truthful.
What the built-in players handle
The built-in <video> shows the poster image before playback, resolved through nuxt-image so provider-bound posters (Shopify, Cloudinary) work as expected. The built-in <audio> has no native poster, so it renders the cover image above the player. Both emit a <source> per source and a <track> per entry in media.tracks for captions and chapters.
The native elements play progressive sources (a self-contained MP4, WebM, or MP3). They do not switch responsive sources per viewport or demux adaptive streaming (HLS, DASH); that needs a JavaScript player, which is what a custom renderer is for. See Streaming formats.
Overriding with a custom renderer
Register a renderer for a media type to replace the built-in with a streaming player, a branded UI, or anything the native element cannot do. A registered renderer takes precedence over the built-in for its type. Register it from a Nuxt plugin at app root with provideMediaRenderers:
import { defineNuxtPlugin } from '#imports';
import { provideMediaRenderers } from '#ui-kit/components/Media/MediaRenderersProvider';
import VidstackMedia from '../components/VidstackMedia.vue';
export default defineNuxtPlugin((nuxtApp) => {
provideMediaRenderers(nuxtApp.vueApp, {
video: VidstackMedia,
});
});
The map is keyed by media type; register only the types you want to override. Here just video is registered, so audio keeps the built-in native renderer.
The renderer contract
A renderer receives the narrowed media object as its media prop, the playback props (playback, controls, autoplay, muted, loop, playsinline, disablePictureInPicture), and any fallthrough attributes (class, style, data-*) that the call site put on <Media>. A video renderer gets a MediaVideo; an audio renderer gets a MediaAudio. (v-model:paused is wired only to the built-in <video>; a custom renderer owns its own playback state.)
The playback props arrive unresolved: you get the playback mode plus whichever overrides were explicitly set, not the expanded attribute cluster. Map them onto your player yourself, or run them through the exported resolvePlayback helper (#ui-kit/components/Media/resolvePlayback) to get the same mode-default-plus-override precedence the built-in players use.
Declare the playback props you want to honor and map them onto your player. The MediaVideoProps / MediaAudioProps types from #ui-kit/components/Media/types bundle the playback props with a single-type media prop; a renderer that handles both types declares MediaPlaybackProps alongside its own media union (as in the example below). Any playback prop you do not declare falls through as an attribute onto your renderer's root element, so a player with its own UI should declare controls and decide what to do with it rather than let the native attribute land on the root.
Here is a video renderer that wraps Vidstack, typed with MediaVideoProps:
<script setup lang="ts">
import type { MediaVideoProps } from '#ui-kit/components/Media/types';
defineProps<MediaVideoProps>();
</script>
<template>
<media-player
:autoplay="autoplay"
:muted="muted"
:loop="loop"
:playsinline="playsinline"
>
<media-provider>
<source v-for="source in media.sources" :key="source.src" :src="source.src" />
</media-provider>
<media-video-layout />
</media-player>
</template>
controls is left unforwarded on purpose: Vidstack draws its own UI. A renderer reads what it needs from media.sources (and media.tracks, media.streaming) and decides whether a source plays natively or needs a JavaScript player; see Streaming formats.
Sizing is not the renderer's job. Beyond media and the playback props, the renderer receives only fallthrough attributes; the outer box (height, aspect ratio) is set by the Block that wraps <Media>.
Dispatch order
<Media> resolves what to render in this order:
| Asset | What renders |
|---|---|
image | The built-in image renderer |
video / audio with a registered renderer | Your renderer |
video | The built-in native <video> |
audio | The built-in native <audio> |
A registered renderer always wins for its type; otherwise the built-in plays. There is no empty-render path: every asset resolves to a player or an image, so a page stays crawlable with no layout shift.
API Reference
| Prop | Default | Type |
|---|---|---|
media | toMedia("/no-media-provided.svg", 1, 1) | |
alt | stringOverride default alt passed in media object. | |
aspectRatio | string | number | booleanAspect-ratio as string in format 4:3 or 4/3. It's also possible to pass a number.
When passed Note that (Order of types has to stay as is, boolean first. Required for shorthand to work.) | |
sizes | stringSpecify responsive sizes. This is a space-separated list of screen size/width pairs. Uses laioutr-ui breakpoints. | |
retina | false | booleanPass true to enable retina (2x density) images. Disabled by default. |
width | string | numberSpecify width of the image | |
height | string | numberSpecify height of the image | |
loading | "lazy" | "eager"This is a native attribute that provides a hint to the browser on how to handle the loading of an image which is outside the viewport. Defaults to 'lazy' for performance reasons. | |
preload | false | booleanIn case you want to preload the image, use this prop. This will place a corresponding |
fetchpriority | "auto" | "low" | "high"This is a native attribute that provides a hint to the browser on how to prioritize the loading of an image. This value is also used for the | |
desktopBreakpoint | md | number | BreakpointName | (string & {})Specify the breakpoint at which the image should be displayed as a desktop image. Can be either a pre-defined breakpoint key (like 'lg') or a number (width in pixels). |
Changelog
<Media> gained a playback mode for video. playback="background" is a one-switch decorative loop (autoplay, muted, loop, playsinline, disablePictureInPicture on, controls off); interactive (the default) is the native player. Each attribute still has its own overriding prop. <Media> now exposes v-model:paused so consumers render their own WCAG 2.2.2 pause control, and suppresses autoplay under reduced motion. See Background video.
<Media> is now a dispatcher that renders video and audio, not only images. Built-in native <video> and <audio> players handle progressive sources out of the box (the video poster and audio cover are shown automatically), and playback is set with props on <Media> (controls, autoplay, muted, loop, playsinline). Register a renderer with provideMediaRenderers to override the built-in for adaptive streaming (HLS/DASH) or a custom player UI. See Rendering video and audio.