---
title: "MediaWrapper"
description: "Play affordance and overlays: MediaWrapper, MediaOverlay, MediaOverlayIcon."
requested_language: sk
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/sk/next/components/media-wrapper
version: unreleased (main)
docs_index: https://docs.systhema.app/sk/next/llms.txt
---
> This page isn't translated yet. Showing English.


`MediaWrapper` frames media in the same border, radius and aspect ratio as [`Image`](https://docs.systhema.app/sk/next/components/image.md), and turns a poster into a play button. `MediaOverlay` puts a poster image over a video or an embed, and `MediaOverlayIcon` draws the play icon on it. Activating the wrapper hides the overlay and starts the video.

```tsx preview iframe height=440 title="MediaWrapper"
import { Image, MediaOverlay, MediaOverlayIcon, MediaWrapper, Video } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="w-full max-w-3xl">
      <MediaWrapper aspectRatio="16/9" playAffordance>
        <Video src="/demo-assets/loop.mp4" controls deferControls deferControlsLabel={null} />
        <MediaOverlay>
          <Image src="/demo-assets/loop-poster.webp" alt="" width={1280} height={720} />
          <MediaOverlayIcon />
        </MediaOverlay>
      </MediaWrapper>
    </div>
  )
}
```

## Import

```tsx
import { MediaOverlay, MediaOverlayIcon, MediaWrapper } from '@systhemaui/next'
```

In a React app without Next.js, import them from `@systhemaui/react`.

## Variants and tags

| Component          | Renders                                                                                   |
| ------------------ | ----------------------------------------------------------------------------------------- |
| `MediaWrapper`     | a `<div>`, or the element in `as`; the play affordance when it wraps something playable   |
| `MediaWrapper.a`   | a link through the [`LinkHelper`](https://docs.systhema.app/sk/next/components/utilities.md#linkhelper); clicks navigate, never play |
| `MediaOverlay`     | a `<div>` layered over the media, faded out once the wrapper plays                        |
| `MediaOverlayIcon` | a `<span>` centered on the overlay; empty, it draws the play icon                         |

## The play affordance

The wrapper is a play button only when it wraps something playable. It inspects its direct children (and any fragments or arrays around them) and takes `role="button"`, `tabIndex={0}` and an `aria-label` only when it finds a `<Video>`, a `<MediaOverlay>` poster or a plain `<video>`. That matches where core's CSS shows `cursor: pointer`. A still image, a map or another embed stays out of the tab order instead of announcing a "Play video" button that does nothing.

When activated (click, Enter or Space on the wrapper itself), the wrapper:

1. sets `data-playing="true"`, which fades the `MediaOverlay` out and lets clicks through to the media,
2. calls `onPlayChange(true)`,
3. calls `play()` on every `<video>` inside it.

Once playing, the wrapper drops the button role, so the native video controls take over.

Set `playAffordance` for a composition the detector cannot see through: a custom component that renders a `<video>` internally, or any wrapper you render in a Server Component. There the `Video` and `MediaOverlay` children reach the wrapper as client references, which the detector does not recognize, so the examples on this page that have no hooks set `playAffordance` explicitly. `playAffordance={false}` states that a wrapper is inert.

## Examples

### Poster over an embed

An embed you cannot script, such as a cross-origin `<iframe>`, can still sit behind a poster. Listen to `onPlayChange` and mount the embed when the visitor activates the wrapper, so its scripts load only on demand.

```tsx preview iframe height=440 title="Poster, then embed"
import { useState } from 'react'
import { Image, MediaOverlay, MediaOverlayIcon, MediaWrapper, Video } from '@systhemaui/next'

export default function Demo() {
  const [playing, setPlaying] = useState(false)

  return (
    <div className="w-full max-w-3xl">
      <MediaWrapper aspectRatio="16/9" onPlayChange={setPlaying} playVideoLabel="Play the tour">
        {playing ? <Video src="/demo-assets/loop.mp4" autoPlay controls /> : null}
        <MediaOverlay>
          <Image src="/demo-assets/workspace.webp" alt="" width={1800} height={1200} />
          <MediaOverlayIcon />
        </MediaOverlay>
      </MediaWrapper>
    </div>
  )
}
```

In a real page, swap the `Video` for the embed's `<iframe>` with its autoplay parameter.

### Linked media

`MediaWrapper.a` makes the whole frame a link, for example to an article. It is never a play button. Inside an overlay, the poster zooms in slightly on hover.

```tsx preview iframe height=360 title="MediaWrapper.a"
import { Image, MediaOverlay, MediaWrapper } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="w-full max-w-md">
      <MediaWrapper.a href="#linked-media" aspectRatio="4/3" aria-label="Read the coast story">
        <MediaOverlay>
          <Image src="/demo-assets/coast.webp" alt="" width={1800} height={1200} />
        </MediaOverlay>
      </MediaWrapper.a>
    </div>
  )
}
```

### Custom play icon

Put your own icon in `MediaOverlayIcon`, or change the default for the whole project with `blocks.media.overlayIcon` (see [Component CSS blocks](https://docs.systhema.app/sk/next/styling/blocks.md)). The icon is sized by `--media-icon-size`, and the overlay tint comes from `--color-media-overlay`.

```tsx preview iframe height=440 title="Custom icon"
import { Image, MediaOverlay, MediaOverlayIcon, MediaWrapper, Video } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="w-full max-w-3xl">
      <MediaWrapper aspectRatio="16/9" playAffordance>
        <Video src="/demo-assets/loop.mp4" controls deferControls deferControlsLabel={null} />
        <MediaOverlay>
          <Image src="/demo-assets/loop-poster.webp" alt="" width={1280} height={720} />
          <MediaOverlayIcon>
            <svg viewBox="0 -960 960 960" fill="currentColor" aria-hidden="true" focusable="false">
              <path d="m380-300 280-180-280-180v360ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z" />
            </svg>
          </MediaOverlayIcon>
        </MediaOverlay>
      </MediaWrapper>
    </div>
  )
}
```

## Props

### `MediaWrapper`

`playVideoLabel` defaults to `'Play video'` and names the affordance while the video is paused. A blank value falls back to the default, so the button is never nameless; unlike [`Video`'s `deferControlsLabel`](https://docs.systhema.app/sk/next/components/video.md#defercontrolslabel) it has no `null` opt-out, because the wrapper is the affordance. `isPlaying` makes the play state controlled. `disableAnimation` drops the configured `animationClasses`; the wrapper also switches off the scroll reveals of the media inside it, so the frame reveals as one piece.

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

| 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`                                                                                                                                                                                                                                         | -       |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `isPlaying`                 | `boolean`                                                                                                                                                                                                                                         | -       |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `onPlayChange`              | `((isPlaying: boolean) => void)`                                                                                                                                                                                                                  | -       |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `playVideoLabel`            | `string`                                                                                                                                                                                                                                          | -       | Accessible name for the play affordance the wrapper itself becomes while its video is paused (`role="button"` + `tabIndex={0}`). Unlike `<Video deferControlsLabel>`, this has no `null` opt-out: the wrapper IS the affordance, so there is no ancestor to defer the labelling to, and dropping the name would leave a nameless `role="button"`. A blank value therefore falls back to the English default rather than clearing it.                                                                                                                                                                                                  |
| `playAffordance`            | `boolean`                                                                                                                                                                                                                                         | -       | Whether the wrapper renders as the play affordance (`role="button"`, `tabIndex={0}` and an accessible name) for its media. Left `undefined` this is auto-detected: the wrapper is an affordance only when it actually wraps a `<Video>` or a `<MediaOverlay>` poster. Without that gate a still image or a bare embed became a focusable button that announced "Play video" and did nothing when activated. Set it explicitly for a composition the detector cannot see through — e.g. `playAffordance` on a wrapper whose custom child renders a `<video>` internally, or `playAffordance={false}` to state that a wrapper is inert. |
| `children` (required)       | `ReactNode`                                                                                                                                                                                                                                       | -       |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `as`                        | `ElementType`                                                                                                                                                                                                                                     | `'div'` |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| …and all `<div>` attributes |                                                                                                                                                                                                                                                   |         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |

<!-- /generated -->

### `MediaOverlay` and `MediaOverlayIcon`

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

| Prop        | Type        | Default | Description |
| ----------- | ----------- | ------- | ----------- |
| `className` | `string`    | -       |             |
| `children`  | `ReactNode` | -       |             |

<!-- /generated -->

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

| Prop                         | Type                       | Default  | Description |
| ---------------------------- | -------------------------- | -------- | ----------- |
| `className`                  | `string`                   | -        |             |
| `as`                         | `'div' \| 'span' \| 'svg'` | `'span'` |             |
| …and all `<span>` attributes |                            |          |             |

<!-- /generated -->

## HTML and CSS

The same structure with classes. Set `data-playing="true"` on the wrapper (from your own script) to fade the overlay out:

```html
<div class="media-wrapper aspect-16/9" role="button" tabindex="0" aria-label="Play video">
  <video class="media object-cover" src="/loop.mp4" preload="metadata" playsinline></video>
  <div class="media-overlay">
    <img src="/loop-poster.webp" alt="" />
    <span class="media-overlay-icon"></span>
  </div>
</div>
```

An empty `.media-overlay-icon` draws the default play icon. While a wrapper with an overlay is not playing, it shows a pointer cursor.

<!-- 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` twin behaves the same, with two differences:

- `MediaWrapper.a` renders through the Next-aware [`LinkHelper`](https://docs.systhema.app/sk/next/components/utilities.md#linkhelper), so the link prefetches and navigates on the client.
- Without `playVideoLabel`, the label comes from the Systhema frontend message catalog (`accessibility.playVideo`), so it follows the page locale when [frontend locales](https://docs.systhema.app/sk/next/nextjs/locales.md) are configured.

Children are matched by element identity first and then by display name, so a `Video` imported from `@systhemaui/react` inside the Next wrapper still counts as playable. The module that defines `MediaWrapper` is a server module that delegates to client components, so `MediaWrapper.a` works when imported in a Server Component.

## Accessibility

- The wrapper is a button only while it wraps something playable and is not yet playing. It activates on Enter and Space as well as click, and ignores key presses that bubble up from controls inside it.
- Give a wrapped `Video` with deferred controls `deferControlsLabel={null}`, so the action has one name, the wrapper's.
- Localize `playVideoLabel` in React; in Next.js it comes from the message catalog.
- A `MediaWrapper.a` with only an image inside needs a name: an `aria-label`, or `alt` text on the image.

## Related

- [Video](https://docs.systhema.app/sk/next/components/video.md)
- [Image](https://docs.systhema.app/sk/next/components/image.md)
- [Video block](https://docs.systhema.app/sk/next/payload/blocks/video.md)
