Interactive

Video Modal

A modal that mounts a native video element, plays it when it opens and pauses it when it closes.

A UModal whose content is a native <video controls> with a spring entrance. The part worth knowing is not the animation but the lifecycle: the element only exists while the modal is open, so the component plays it on open and pauses it on close, driven by a watcher on open.

Because playback is triggered imperatively rather than by the autoplay attribute alone, closing the modal genuinely stops the audio instead of leaving it running behind the overlay.

The open watcher is not immediate, so a modal that is already open on first render does not call play() — it waits for the first change to open.

Usage

Drive open yourself and pass a src; the trigger goes in the default slot. A poster is worth setting, because it is the frame the user sees during the entrance and while the browser fetches the first chunk.

The element mounts with the modal and is told to play on open, pause on close.

<template>
  <div class="flex w-full max-w-md flex-col gap-4">
    <MVideoModal
      v-model:open="open"
      :src="src"
      :poster="poster"
    >
      <UButton
        label="Watch the tour"
        icon="i-lucide-play"
      />
    </MVideoModal>

    <p class="text-xs text-muted">
      The element mounts with the modal and is told to play on open, pause on close.
    </p>
  </div>
</template>

<script setup lang="ts">
const open = ref(false)

const src = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4'
const poster = 'https://picsum.photos/seed/video-modal-poster/800/450'
</script>

Customize

Customize

Srchttps://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4
Posterhttps://picsum.photos/seed/video-modal-poster/800/450
Autoplay
Open

Props

Prop Type Default Description
srcstring-Required. Set as the src of the <video> element, which is created with the modal's content — so changing it while the modal is closed costs nothing, and changing it while open swaps the source in place.
posterstring''The poster frame. Set as the native poster attribute and shown before playback starts; also what is visible during the entrance spring.
autoplaybooleanfalseSet as the native autoplay attribute. Note that it is effectively decorative here: the open watcher calls play() unconditionally whenever the modal opens, regardless of this value, so the video starts either way. The rejection is swallowed with .catch(() => {}).
openbooleanfalseControlled open state. The component holds no state of its own here — it forwards to UModal and emits update:open back out, so v-model:open is the intended usage.

Events

Event Payload Description
update:open(value: boolean) => voidEmitted from the underlying modal. Dismissal through the overlay, Escape, the close button, or the trigger all arrive here — this is what makes v-model:open work.

Slots

Slot Props Description
default-The trigger, forwarded to UModal's default slot. It is the only thing you can put here; the panel itself is the video element and takes no slot.

UI

This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.videoModal in app.config.ts.

Prop Type Default Description
uiobject-Per-slot classes merged over the theme defaults.
Slot Props Description
root-
container-relative
video-w-full rounded-lg

Accessibility

Semantics are entirely UModal's: role="dialog", aria-modal, a focus trap, Escape to dismiss, and focus restored to the trigger on close. The video renders with the native controls attribute, so playback, seeking, volume, fullscreen and captions are all keyboard-operable without any extra work.

What the component does not do is label or caption the video. There is no aria-label, no <track> support and no way to pass attributes to the element — it is rendered from a fixed set of attributes, so if your video has captions you will need to reach past this component. Pair it with a transcript link outside the modal.

There is no reduced-motion branch. The entrance spring (stiffness: 300, damping: 25, scaling from 0.95) and the matching exit play unconditionally.

Performance

  • The <video> element is created with the modal's content and destroyed with it, so a closed modal holds no decoder, no buffer and no network connection.
  • Playback control runs through a nextTick before touching the ref, so the element is guaranteed to exist on the tick the watcher fires — and the code bails out cleanly if it does not.
  • The rejected-play() promise is caught and ignored, which keeps a blocked unmuted autoplay from surfacing as an unhandled rejection in the console.
  • opacity and scale only; no layout property is animated, so opening the modal does not reflow the page behind it.
play() is called on every open with no muted and no user-gesture check, and browsers block unmuted programmatic playback that was not preceded by a click. In practice the user clicks the trigger first, which usually satisfies the policy — but if the modal is opened programmatically (a route change, a notification tap), the promise rejects silently and the poster simply stays until the user presses play.
Copyright © 2026