Dialog
A modal dialog for application content, including players opened from a thumbnail
Import
import { Dialog } from '@videojs/react';import '@videojs/html/ui/dialog';Anatomy
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Backdrop />
<Dialog.Popup>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
</Dialog.Popup>
</Dialog.Root><button commandfor="video-dialog">Open video</button>
<media-dialog id="video-dialog">
<media-dialog-backdrop></media-dialog-backdrop>
<media-dialog-popup>
<media-dialog-title></media-dialog-title>
<media-dialog-description></media-dialog-description>
<media-dialog-close></media-dialog-close>
</media-dialog-popup>
</media-dialog>Behavior
Dialog provides the modal interaction and accessibility behavior while your application supplies the content and styling. It is the general-purpose replacement for a ModalDialog use case such as opening a player from a thumbnail.
Use Dialog.Trigger to open the dialog. Use open with onOpenChange for controlled state, or defaultOpen for uncontrolled state. Root provides state without rendering an element, while Popup renders only while the dialog is open or closing.
Place a button immediately before media-dialog, or connect them explicitly with commandfor and an id. You can also set the dialog’s open property directly. The root provides state without creating a layout box; media-dialog-backdrop and media-dialog-popup are sibling surfaces. The element dispatches open-change when its state changes and open-change-complete after the popup transition finishes.
The close part and Escape close the dialog. Other buttons inside the dialog, including player controls, do not close it.
Your application decides what opening and closing mean for the media. The example starts playback after opening and pauses it as soon as the dialog closes. Handle the promise returned by play(), since browsers can reject autoplay when it is not allowed.
Use AlertDialog for urgent messages that require acknowledgement. ErrorDialog is the player-error specialization. Both use the same dialog behavior.
Styling
Use the open and transition data attributes to style the modal and its animations:
| Attribute | Description |
|---|---|
data-open |
Present while the dialog is open, including its closing transition |
data-starting-style |
Present at the start of the opening transition |
data-ending-style |
Present during the closing transition |
media-dialog-backdrop:not([data-open]),
media-dialog-popup:not([data-open]) {
display: none;
}
media-dialog-backdrop[data-starting-style],
media-dialog-backdrop[data-ending-style],
media-dialog-popup[data-starting-style],
media-dialog-popup[data-ending-style] {
opacity: 0;
}React renders the backdrop and popup as separate DOM elements. Add a className to each part and style their transitions independently:
.dialog-backdrop[data-starting-style],
.dialog-backdrop[data-ending-style],
.dialog-popup[data-starting-style],
.dialog-popup[data-ending-style] {
opacity: 0;
}Accessibility
The popup uses role="dialog" and aria-modal="true". Include Dialog.Title so the dialog always has an accessible name. Dialog.Description is optional. These parts receive generated IDs connected through aria-labelledby and aria-describedby. The trigger receives aria-haspopup, aria-controls, and aria-expanded.
When the dialog opens, focus moves to an element with autofocus, the first focusable element, or the popup itself. Tab and Shift + Tab keep focus within the dialog, and content outside the dialog becomes inert. Closing restores focus to the trigger, or to the element that was focused before the dialog opened.
Dialogs inside the same player container share modal coordination. Opening one closes the current dialog and dismisses any open menu, tooltip, or popover before focus moves into the new popup.
Examples
Player modal
Open the video from its thumbnail. Playback starts when the dialog opens and pauses when it closes.
import { Container, createPlayer, Dialog } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
import { useEffect, useRef, useState } from 'react';
import './BasicUsage.css';
const { Player } = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
const [open, setOpen] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
if (open) void video.play().catch(() => {});
else video.pause();
}, [open]);
return (
<div className="react-dialog-basic">
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger className="react-dialog-basic__trigger">
<img src="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.jpg" alt="" />
<span>Play the video</span>
</Dialog.Trigger>
<Dialog.Backdrop className="react-dialog-basic__backdrop" />
<Dialog.Popup className="react-dialog-basic__dialog">
<div className="react-dialog-basic__popup">
<Dialog.Title className="react-dialog-basic__title">Video title</Dialog.Title>
<Dialog.Description className="react-dialog-basic__description">
A video opened from a thumbnail.
</Dialog.Description>
<Dialog.Close className="react-dialog-basic__close">Close</Dialog.Close>
<Player>
<Container>
<Video ref={videoRef} src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" controls muted playsInline />
</Container>
</Player>
</div>
</Dialog.Popup>
</Dialog.Root>
</div>
);
}
.react-dialog-basic {
position: relative;
min-height: 320px;
}
.react-dialog-basic__trigger {
display: grid;
gap: 10px;
width: min(100%, 420px);
padding: 0 0 12px;
overflow: hidden;
color: white;
cursor: pointer;
background: #111827;
border: 0;
border-radius: 12px;
}
.react-dialog-basic__trigger img {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.react-dialog-basic__backdrop {
position: absolute;
inset: 0;
z-index: 1;
background: rgb(3 7 18 / 80%);
transition: opacity 200ms ease;
}
.react-dialog-basic__dialog {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
transition: opacity 150ms ease;
}
.react-dialog-basic__backdrop[data-starting-style],
.react-dialog-basic__backdrop[data-ending-style],
.react-dialog-basic__dialog[data-starting-style],
.react-dialog-basic__dialog[data-ending-style] {
opacity: 0;
}
.react-dialog-basic__popup {
position: relative;
width: 100%;
max-width: 560px;
padding: 16px;
color: white;
background: #111827;
border-radius: 12px;
}
.react-dialog-basic__title {
margin: 0 0 4px;
font-size: 1.125rem;
font-weight: 600;
}
.react-dialog-basic__description {
margin: 0 0 12px;
}
.react-dialog-basic__close {
position: absolute;
top: 12px;
right: 12px;
padding: 6px 12px;
color: #111827;
cursor: pointer;
background: white;
border: 0;
border-radius: 9999px;
}
.react-dialog-basic video {
display: block;
width: 100%;
}
@media (prefers-reduced-motion: reduce) {
.react-dialog-basic__backdrop,
.react-dialog-basic__dialog {
transition: none;
}
}
<div class="html-dialog-basic">
<button class="html-dialog-basic__trigger" type="button" commandfor="html-video-dialog">
<img src="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.jpg" alt="" />
<span>Play the video</span>
</button>
<media-dialog id="html-video-dialog">
<media-dialog-backdrop class="html-dialog-basic__backdrop"></media-dialog-backdrop>
<media-dialog-popup class="html-dialog-basic__dialog">
<div class="html-dialog-basic__popup">
<media-dialog-title class="html-dialog-basic__title">Video title</media-dialog-title>
<media-dialog-description class="html-dialog-basic__description">
A video opened from a thumbnail.
</media-dialog-description>
<media-dialog-close class="html-dialog-basic__close">Close</media-dialog-close>
<video-player>
<media-container>
<video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" controls muted playsinline></video>
</media-container>
</video-player>
</div>
</media-dialog-popup>
</media-dialog>
</div>
.html-dialog-basic {
position: relative;
min-height: 320px;
}
.html-dialog-basic__trigger {
display: grid;
gap: 10px;
width: min(100%, 420px);
padding: 0 0 12px;
overflow: hidden;
color: white;
cursor: pointer;
background: #111827;
border: 0;
border-radius: 12px;
}
.html-dialog-basic__trigger img {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.html-dialog-basic__backdrop {
position: absolute;
inset: 0;
z-index: 1;
background: rgb(3 7 18 / 80%);
transition: opacity 200ms ease;
}
.html-dialog-basic__dialog {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
transition: opacity 150ms ease;
}
.html-dialog-basic__backdrop:not([data-open]),
.html-dialog-basic__dialog:not([data-open]) {
display: none;
}
.html-dialog-basic__backdrop[data-starting-style],
.html-dialog-basic__backdrop[data-ending-style],
.html-dialog-basic__dialog[data-starting-style],
.html-dialog-basic__dialog[data-ending-style] {
opacity: 0;
}
.html-dialog-basic__popup {
position: relative;
width: 100%;
max-width: 560px;
padding: 16px;
color: white;
background: #111827;
border-radius: 12px;
}
.html-dialog-basic__title {
display: block;
margin-bottom: 4px;
font-size: 1.125rem;
font-weight: 600;
}
.html-dialog-basic__description {
display: block;
margin-bottom: 12px;
}
.html-dialog-basic__close {
position: absolute;
top: 12px;
right: 12px;
padding: 6px 12px;
color: #111827;
cursor: pointer;
background: white;
border: 0;
border-radius: 9999px;
}
.html-dialog-basic video {
display: block;
width: 100%;
}
@media (prefers-reduced-motion: reduce) {
.html-dialog-basic__backdrop,
.html-dialog-basic__dialog {
transition: none;
}
}
import '@videojs/html/video/player';
import '@videojs/html/ui/dialog';
document.querySelectorAll<HTMLElement>('.html-dialog-basic').forEach((demo) => {
const dialog = demo.querySelector('media-dialog');
const video = demo.querySelector('video');
dialog?.addEventListener('open-change', (event) => {
if (!video) return;
const { open } = (event as CustomEvent<{ open: boolean }>).detail;
if (open) void video.play().catch(() => {});
else video.pause();
});
});
API Reference
Rootmedia-dialog
Manages dialog state and provides it to the compound parts.
Props
| Prop | Type | Default | Details |
|---|---|---|---|
closeOnEscape | boolean | true | |
| |||
defaultOpen | boolean | false | |
| |||
open | boolean | false | |
| |||
State
render, className, and style props.| Property | Type | Details |
|---|---|---|
transitionStarting | boolean | |
| ||
transitionEnding | boolean | |
| ||
open | boolean | |
| ||
status | 'idle' | 'starting' | 'ending' | |
| ||
titleId | string | undefined | |
| ||
descriptionId | string | undefined | |
| ||
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-open | ||
| ||
data-starting-style | ||
| ||
data-ending-style | ||
| ||
Events
| Event | Description |
|---|---|
open-change | |
open-change-complete |
Backdropmedia-dialog-backdrop
Presentational layer behind a dialog while it is rendered, including its exit transition.
Props
| Prop | Type | Default | Details |
|---|---|---|---|
className | string | ((state: DialogCore.State) => string | undefined) | — | |
| |||
render | ReactElement | ((props: HTMLProps, state: DialogCore.State) => ReactElement | null) | — | |
| |||
style | CSSProperties | ((state: DialogCore.State) => CSSProperties | undefined) | — | |
| |||
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-open | ||
| ||
data-starting-style | ||
| ||
data-ending-style | ||
| ||
Closemedia-dialog-close
Renders a button that closes the dialog.
Props
| Prop | Type | Default | Details |
|---|---|---|---|
className | string | ((state: DialogCore.State) => string | undefined) | — | |
| |||
render | ReactElement | ((props: HTMLProps, state: DialogCore.State) => ReactElement | null) | — | |
| |||
style | CSSProperties | ((state: DialogCore.State) => CSSProperties | undefined) | — | |
| |||
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-open | ||
| ||
data-starting-style | ||
| ||
data-ending-style | ||
| ||
Descriptionmedia-dialog-description
Renders the description announced with the dialog.
Props
| Prop | Type | Default | Details |
|---|---|---|---|
className | string | ((state: DialogCore.State) => string | undefined) | — | |
| |||
render | ReactElement | ((props: HTMLProps, state: DialogCore.State) => ReactElement | null) | — | |
| |||
style | CSSProperties | ((state: DialogCore.State) => CSSProperties | undefined) | — | |
| |||
Popupmedia-dialog-popup
Renders the modal dialog while it is open.
Props
| Prop | Type | Default | Details |
|---|---|---|---|
className | string | ((state: DialogCore.State) => string | undefined) | — | |
| |||
render | ReactElement | ((props: HTMLProps, state: DialogCore.State) => ReactElement | null) | — | |
| |||
style | CSSProperties | ((state: DialogCore.State) => CSSProperties | undefined) | — | |
| |||
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-open | ||
| ||
data-starting-style | ||
| ||
data-ending-style | ||
| ||
Titlemedia-dialog-title
Renders the heading that labels the dialog.
Props
| Prop | Type | Default | Details |
|---|---|---|---|
className | string | ((state: DialogCore.State) => string | undefined) | — | |
| |||
render | ReactElement | ((props: HTMLProps, state: DialogCore.State) => ReactElement | null) | — | |
| |||
style | CSSProperties | ((state: DialogCore.State) => CSSProperties | undefined) | — | |
| |||
TriggerTrigger
Renders a button that opens the dialog.
Props
| Prop | Type | Default | Details |
|---|---|---|---|
className | string | ((state: DialogCore.State) => string | undefined) | — | |
| |||
render | ReactElement | ((props: HTMLProps, state: DialogCore.State) => ReactElement | null) | — | |
| |||
style | CSSProperties | ((state: DialogCore.State) => CSSProperties | undefined) | — | |
| |||
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-open | ||
| ||
data-starting-style | ||
| ||
data-ending-style | ||
| ||