Docs

This page isn't translated yet

Video

Videos with deferred controls, preload and parallax.

On this page

Video renders a <video> with the same media props as Image: aspect ratio, object fit and position, and parallax. It also keeps the cost of a video low before anyone watches it: it preloads only metadata, and it can hold the native controls back until the visitor interacts, so they never shift the layout.

import { Video } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="w-full max-w-3xl">
      <Video
        src="/demo-assets/loop.mp4"
        poster="/demo-assets/loop-poster.webp"
        aspectRatio="16/9"
        autoPlay
      />
    </div>
  )
}

ImportLink to this section

import { Video } from '@systhemaui/next'

In a React app without Next.js, import it from @systhemaui/react. The only difference is the aspectRatio default: '16/9' in React, 'auto' in Next.js.

ExamplesLink to this section

Background loopLink to this section

autoPlay also turns on loop and muted unless you set them, because browsers only autoplay muted video. playsInline defaults to true, so iOS does not open the video full screen.

import { Video } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="w-full max-w-xl">
      <Video src="/demo-assets/loop.mp4" aspectRatio="21/9" autoPlay />
    </div>
  )
}

Native controlsLink to this section

controls shows the browser's control bar. Add a poster so the frame is not blank before the first frame decodes.

import { Video } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="w-full max-w-3xl">
      <Video
        src="/demo-assets/loop.mp4"
        poster="/demo-assets/loop-poster.webp"
        aspectRatio="16/9"
        controls
      />
    </div>
  )
}

Aspect ratio and fitLink to this section

The aspect ratio and objectFit work as on Image. With objectFit="contain" the whole frame stays visible inside a box of another ratio.

cover and contain
import { Paragraph, Video } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="grid w-full max-w-xl grid-cols-2 gap-6">
      <div className="flex flex-col gap-2">
        <Video src="/demo-assets/loop.mp4" aspectRatio="1/1" objectFit="cover" autoPlay />
        <Paragraph type="small">cover</Paragraph>
      </div>
      <div className="flex flex-col gap-2">
        <Video src="/demo-assets/loop.mp4" aspectRatio="1/1" objectFit="contain" autoPlay />
        <Paragraph type="small">contain</Paragraph>
      </div>
    </div>
  )
}

ParallaxLink to this section

parallax works as on Image: the video is wrapped in a parallax-wrapper figure that carries the aspect ratio, and the ParallaxListener moves it as the page scrolls.

import { Video } from '@systhemaui/next'

export function ParallaxBand() {
  return <Video src="/videos/band.mp4" aspectRatio="21/9" autoPlay parallax />
}

Loading and deferred controlsLink to this section

Three props decide what a video costs before anyone watches it.

preloadLink to this section

preload defaults to 'metadata': the browser reads the duration and paints the first frame without fetching the stream. 'auto' downloads the whole file during page load, which for a large hero video can be tens of megabytes inside the LCP window. 'none' fetches nothing until playback.

deferControlsLink to this section

A <video controls> draws its control bar only once metadata has loaded, so the bar appears over the video after the page has settled: a layout shift nobody asked for (0.185 CLS on one measured production page, its entire CLS). Shifts within 500 ms of a discrete user input do not count toward CLS, so deferControls holds the bar back until the visitor interacts and then reveals it for free.

Click the video below, or focus it and press Enter: the native controls appear and it starts playing.

Deferred controls, click to play
import { Video } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="w-full max-w-3xl">
      <Video
        src="/demo-assets/loop.mp4"
        poster="/demo-assets/loop-poster.webp"
        aspectRatio="16/9"
        controls
        deferControls
      />
    </div>
  )
}

What counts as the first interaction depends on how the video plays:

  • A click-to-play video activates on a click (which every pointer, touch and assistive-technology activation produces), on Enter or Space, and on its play event, so an outside trigger such as a MediaWrapper poster works too. While its controls are deferred, the video itself is the play button: role="button", tabIndex={0} and the deferControlsLabel name.
  • An autoplaying video activates on the first completed discrete input anywhere on the page: a pointer release, a key press (the first Tab included), or the click an assistive technology sends. It is not a play button, so it gets no role="button". The trigger is the pointer release rather than the press, because a press that turns into a scroll is not a discrete input for CLS.
Deferred controls, autoplay
import { Video } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="w-full max-w-3xl">
      <Video
        src="/demo-assets/loop.mp4"
        poster="/demo-assets/loop-poster.webp"
        aspectRatio="16/9"
        autoPlay
        controls
        deferControls
      />
    </div>
  )
}

For autoplaying video this keeps WCAG 2.2.2 (pause, stop, hide) satisfied: anyone who wants it paused has already used an input device, so the focusable native control bar is there when they reach for it.

deferControlsLabelLink to this section

deferControlsLabel is the accessible name of the click-to-play affordance, 'Play video' by default. Translate it with the page. Pass null when an ancestor already provides the affordance, such as a MediaWrapper, which is itself role="button", so the action is not labelled twice. Autoplaying videos ignore it.

The same logic is available on its own as useDeferredVideoControls, which returns { controls, activationProps } to spread onto your own <video>.

PropsLink to this section

Video takes every <video> attribute on top of the media props. disableAnimation drops the configured animationClasses.

PropTypeDefaultDescription
classNamestring-
disableAnimationboolean-
aspectRatioAspectRatio (auto, 32/9, 21/9, 21/9-md, 14/3, 5/2, 3/1, 4/1, 2/1, 16/5, 8/3, 16/9, 16/10, 8/5, 5/4, 4/3, 3/2, 1.618/1, 1/1, 9/8, 2/3, 3/4, 4/5, 6/7, 9/16, 10/16, 1/2, 9/21, 1/1.618)-
parallaxboolean-
parallaxNoWrapperboolean-
objectFit'contain' | 'cover' | null-
objectPosition'top-left' | 'top' | 'top-center' | 'top-right' | 'center-left' | 'left' | 'center-center' | 'center' | 'center-right' | 'right' | 'bottom-left' | 'bottom' | 'bottom-center' | 'bottom-right' | { x?, y? } | null-
deferControlsboolean-Hold the native controls back until the visitor interacts with the video. Off by default; see useDeferredVideoControls for why it exists.
deferControlsLabelstring | null-Accessible name for the play affordance the video carries while its controls are deferred. null opts out when an ancestor (e.g. <MediaWrapper>) already provides one. Ignored for autoplaying videos, which are not play affordances.
…and all <video> attributes

HTML and CSSLink to this section

Without React, combine media with an aspect-ratio and an object-fit class:

<video
  class="media aspect-16/9 object-cover object-center"
  src="/loop.mp4"
  poster="/loop-poster.webp"
  preload="metadata"
  playsinline
  controls
></video>

<video
  class="media aspect-21/9 object-cover"
  src="/loop.mp4"
  autoplay
  muted
  loop
  playsinline
  preload="metadata"
></video>

Deferred controls are JavaScript; in plain HTML, add controls directly.

ClassStyles
media.hero-simple-media:is(.media) { max-width: unset; left: 50%; translate: -50% 0; width: var(--hero-simple-media-width, 100%); }.hero-background-media:is(.media) { z-index: 5; position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; border-radius: 0px; box-shadow: none; }.hero-background-media:is(.media) > * { width: 100%; height: 100%; border-radius: 0px; box-shadow: none; }+ 11 more rules
media-wrapper.hero-simple-media:is(.media-wrapper) { max-width: unset; left: 50%; translate: -50% 0; width: var(--hero-simple-media-width, 100%); }.hero-background-media:is(.media-wrapper) { z-index: 5; position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; border-radius: 0px; box-shadow: none; }.hero-background-media:is(.media-wrapper) > * { width: 100%; height: 100%; border-radius: 0px; box-shadow: none; }+ 12 more rules
media-overlay& { z-index: 1; position: absolute; inset: 0; opacity: 1; pointer-events: none; transition-property: opacity; transition-duration: 500ms; transition-timing-function: ease-out; color: var(--color-media-icon); }.media:has(video) .media-overlay { pointer-events: auto; }.media-wrapper:has(video) .media-overlay { pointer-events: auto; }[data-playing="true"] .media-overlay { opacity: 0; pointer-events: none; }+ 7 more rules
media-overlay-icon& { position: absolute; top: 50%; left: 50%; translate: -50% -50%; color: var(--color-media-icon); box-shadow: inset 0 0 0 100vmax var(--color-media-overlay), 0 0 0 100vmax var(--color-media-overlay); width: var(--media-icon-size); height: var(--media-icon-size); font-size: var(--media-icon-size); line-height: var(--media-icon-size); }+ 1 more rule

Next.jsLink to this section

The @systhemaui/next Video matches the React one, including preload and the deferred controls. One difference: aspectRatio defaults to 'auto' instead of '16/9', because Next.js projects render hero and background videos at their intrinsic size.

AccessibilityLink to this section

  • Deferred controls keep the video keyboard-operable before the native bar exists, as described in deferControls.
  • An autoplaying video is muted and loops. Keep the native controls (deferred or not), so visitors can pause it.
  • The Payload Video block has no caption track field yet. When you render your own video with speech, add a <track kind="captions"> child.

See Embeds and viewport and Reduced motion.