---
title: "Video"
description: "Videos with deferred controls, preload and parallax."
requested_language: cs
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/cs/components/video
version: unreleased (main)
docs_index: https://docs.systhema.app/cs/llms.txt
---
> This page isn't translated yet. Showing English.


`Video` renders a `<video>` with the same media props as [`Image`](https://docs.systhema.app/cs/components/image.md): 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.

```tsx preview iframe height=420 title="Video"
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>
  )
}
```

## Import

```tsx
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.

## Examples

### Background loop

`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.

```tsx preview iframe height=360 title="Autoplay loop"
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 controls

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

```tsx preview iframe height=420 title="Controls"
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 fit

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

```tsx preview iframe height=360 title="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>
  )
}
```

### Parallax

`parallax` works as on [`Image`](https://docs.systhema.app/cs/components/image.md#parallax): the video is wrapped in a `parallax-wrapper` figure that carries the aspect ratio, and the [`ParallaxListener`](https://docs.systhema.app/cs/components/listeners.md#parallaxlistener) moves it as the page scrolls.

```tsx
import { Video } from '@systhemaui/next'

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

## Loading and deferred controls

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

### `preload`

`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.

### `deferControls`

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.

```tsx preview iframe height=420 title="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.

```tsx preview iframe height=420 title="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.

### `deferControlsLabel`

`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.

> [!WARNING]
> Deferred markup has no `controls` attribute, so the video needs JavaScript to become playable. For a click-to-play video that only costs the no-JS fallback. For an autoplaying video it is a WCAG 2.2.2 failure without JavaScript: moving content that plays for more than five seconds with no way to pause it. Don't defer an autoplaying video's controls on a page that must work without JavaScript.

The same logic is available on its own as [`useDeferredVideoControls`](https://docs.systhema.app/cs/components/utilities.md#usedeferredvideocontrols), which returns `{ controls, activationProps }` to spread onto your own `<video>`.

## Props

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

<!-- generated:props @systhemaui/react VideoProps -->

| Prop                          | Type                                                                                                                                                                                                                                              | Default | Description                                                                                                                                                                                                                               |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `className`                   | `string`                                                                                                                                                                                                                                          | -       |                                                                                                                                                                                                                                           |
| `disableAnimation`            | `boolean`                                                                                                                                                                                                                                         | -       |                                                                                                                                                                                                                                           |
| `aspectRatio`                 | `AspectRatio` (`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`) | -       |                                                                                                                                                                                                                                           |
| `parallax`                    | `boolean`                                                                                                                                                                                                                                         | -       |                                                                                                                                                                                                                                           |
| `parallaxNoWrapper`           | `boolean`                                                                                                                                                                                                                                         | -       |                                                                                                                                                                                                                                           |
| `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`               | -       |                                                                                                                                                                                                                                           |
| `deferControls`               | `boolean`                                                                                                                                                                                                                                         | -       | Hold the native controls back until the visitor interacts with the video. Off by default; see `useDeferredVideoControls` for why it exists.                                                                                               |
| `deferControlsLabel`          | `string \| 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 |                                                                                                                                                                                                                                                   |         |                                                                                                                                                                                                                                           |

<!-- /generated -->

## HTML and CSS

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

```html
<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.

<!-- generated:utilities media -->

| Class                | Styles                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `media`              | `.hero-simple-media:is(.media) { max-width: unset; left: 50%; translate: -50% 0; width: var(--hero-simple-media-width, 100%); }`<br>`.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; }`<br>`.hero-background-media:is(.media)  > * { width: 100%; height: 100%; border-radius: 0px; box-shadow: none; }`<br>+ 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%); }`<br>`.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; }`<br>`.hero-background-media:is(.media-wrapper)  > * { width: 100%; height: 100%; border-radius: 0px; box-shadow: none; }`<br>+ 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); }`<br>`.media:has(video) .media-overlay { pointer-events: auto; }`<br>`.media-wrapper:has(video) .media-overlay { pointer-events: auto; }`<br>`[data-playing="true"] .media-overlay { opacity: 0; pointer-events: none; }`<br>+ 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); }`<br>+ 1 more rule                                                                                                                           |

<!-- /generated -->

## Next.js

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.

## Accessibility

- Deferred controls keep the video keyboard-operable before the native bar exists, as described in [`deferControls`](#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](https://docs.systhema.app/cs/concepts/accessibility.md#embeds-and-viewport) and [Reduced motion](https://docs.systhema.app/cs/concepts/accessibility.md#reduced-motion).

## Related

- [MediaWrapper](https://docs.systhema.app/cs/components/media-wrapper.md)
- [Image](https://docs.systhema.app/cs/components/image.md)
- [Video block](https://docs.systhema.app/cs/payload/blocks/video.md)
