Video Player Dialog
Composable modal structure for video playback, metadata, and playlists.
Quick Preview
Recommended Architecture
The package exports structural primitives, not a product playback policy. Create one local wrapper—such as AppVideoPlayerDialog—for the composition used throughout your application. Keep API mapping, analytics, active playlist state, and automatic-next behavior in that wrapper.
AppVideoPlayerDialog (local)
└── VideoPlayerDialog
├── VideoPlayerDialogTrigger
└── VideoPlayerDialogContent
├── VideoPlayerDialogMedia
│ ├── MediaPlayer
│ │ ├── MediaPlayerVideo
│ │ ├── MediaPlayerError
│ │ └── any MediaPlayer controls
│ └── VideoPlayerDialogPlaylistIndicator
├── VideoPlayerDialogDetails
│ ├── VideoPlayerDialogTitle
│ └── VideoPlayerDialogDescription
└── VideoPlayerDialogPlaylist
├── VideoPlayerDialogPlaylistHeader
└── VideoPlayerDialogPlaylistList
└── VideoPlayerDialogPlaylistItemUse the local wrapper for the common 90% case. Reach for the package primitives directly only when a screen needs materially different controls, metadata, or playlist composition.
Media Composition
VideoPlayerDialogMedia is deliberately behavior-free. It only provides a responsive 16:9 slot, clipping, and positioning. Place any combination of primitives from @giddaa-housing/ui/media-player inside it.
<VideoPlayerDialogMedia>
<MediaPlayer>
<MediaPlayerVideo src={activeVideo.src} onEnded={playNext} />
<MediaPlayerError />
<MediaPlayerControls>
<MediaPlayerControlsOverlay />
<VideoPlayerDialogPlaylistIndicator current={2} total={5} />
<MediaPlayerSeek />
<MediaPlayerPlay />
<MediaPlayerVolume />
<MediaPlayerCaptions />
<MediaPlayerSettings />
<MediaPlayerFullscreen />
</MediaPlayerControls>
</MediaPlayer>
</VideoPlayerDialogMedia>Adding captions, chapters, settings, playback speed, PiP, custom controls, or error UI does not require changing the dialog primitives.
Local Wrapper
The live examples use a docs-local AppVideoPlayerDialog recipe. It maps application video data into the primitives, owns playlist selection, and optionally advances after ended. Copy that pattern into the consuming application and adapt it there.
"use client";import { MediaPlayer, MediaPlayerControls, MediaPlayerControlsOverlay, MediaPlayerError, MediaPlayerFullscreen, MediaPlayerPlay, MediaPlayerSeek, MediaPlayerTime, MediaPlayerVideo, MediaPlayerVolume,} from "@giddaa-housing/ui/media-player";import { VideoPlayerDialog, VideoPlayerDialogContent, VideoPlayerDialogDescription, VideoPlayerDialogDetails, VideoPlayerDialogMedia, VideoPlayerDialogPlaylist, VideoPlayerDialogPlaylistHeader, VideoPlayerDialogPlaylistIndicator, VideoPlayerDialogPlaylistItem, VideoPlayerDialogPlaylistList, type VideoPlayerDialogSize, type VideoPlayerDialogTheme, VideoPlayerDialogTitle, VideoPlayerDialogTrigger,} from "@giddaa-housing/ui/video-player-dialog";import type { ComponentProps, ReactElement, ReactNode } from "react";import { useEffect, useState } from "react";export interface AppVideoItem { id: string; title: ReactNode; description?: ReactNode; src: string; poster?: string; duration?: ReactNode; videoChildren?: ReactNode;}interface AppVideoPlayerDialogProps extends Omit<ComponentProps<typeof VideoPlayerDialog>, "children"> { trigger: ReactElement; size?: VideoPlayerDialogSize; theme?: VideoPlayerDialogTheme; title?: ReactNode; description?: ReactNode; src?: string; poster?: string; videoChildren?: ReactNode; playlist?: AppVideoItem[]; autoPlayNext?: boolean;}/** * Example application wrapper. Copy this into the consuming app and adapt the * controls, analytics, data mapping, and playback policy locally. */export function AppVideoPlayerDialog({ trigger, size = "md", theme = "light", title, description, src = "", poster, videoChildren, playlist, autoPlayNext = false, ...props}: AppVideoPlayerDialogProps) { const items: AppVideoItem[] = playlist?.length ? playlist : [{ id: "video", title, description, src, poster, videoChildren }]; const [activeIndex, setActiveIndex] = useState(0); const [autoPlayItemId, setAutoPlayItemId] = useState<string>(); const activeItem = items[activeIndex] ?? items[0]; useEffect(() => { if (activeItem?.id === autoPlayItemId) setAutoPlayItemId(undefined); }, [activeItem?.id, autoPlayItemId]); const playNext = () => { if (!autoPlayNext || activeIndex >= items.length - 1) return; const next = items[activeIndex + 1]; if (!next) return; setAutoPlayItemId(next.id); setActiveIndex(activeIndex + 1); }; if (!activeItem) return null; return ( <VideoPlayerDialog {...props}> <VideoPlayerDialogTrigger render={trigger} /> <VideoPlayerDialogContent size={size} theme={theme}> <VideoPlayerDialogMedia> <MediaPlayer> <MediaPlayerVideo key={activeItem.id} autoPlay={activeItem.id === autoPlayItemId} src={activeItem.src} poster={activeItem.poster} onEnded={playNext} > {activeItem.videoChildren} </MediaPlayerVideo> <MediaPlayerError /> <MediaPlayerControls className="inset-0 flex-col items-stretch justify-end gap-0 p-3 sm:p-4"> <MediaPlayerControlsOverlay /> {items.length > 1 ? ( <VideoPlayerDialogPlaylistIndicator current={activeIndex + 1} total={items.length} /> ) : null} <MediaPlayerPlay className="absolute top-1/2 left-1/2 size-14 -translate-x-1/2 -translate-y-1/2 bg-black/50 [&_svg]:size-5" /> <MediaPlayerSeek className="h-5" withTime={false} /> <div className="flex items-center gap-1.5"> <MediaPlayerPlay /> <MediaPlayerVolume /> <MediaPlayerTime className="px-1 text-gdt-xs font-semibold text-white" /> <span className="flex-1" /> <MediaPlayerFullscreen /> </div> </MediaPlayerControls> </MediaPlayer> </VideoPlayerDialogMedia> {activeItem.title || activeItem.description ? ( <VideoPlayerDialogDetails> {activeItem.title ? ( <VideoPlayerDialogTitle> {activeItem.title} </VideoPlayerDialogTitle> ) : null} {activeItem.description ? ( <VideoPlayerDialogDescription> {activeItem.description} </VideoPlayerDialogDescription> ) : null} </VideoPlayerDialogDetails> ) : null} {items.length > 1 ? ( <VideoPlayerDialogPlaylist aria-label="Video playlist"> <VideoPlayerDialogPlaylistHeader> Playlist · {items.length} videos </VideoPlayerDialogPlaylistHeader> <VideoPlayerDialogPlaylistList> {items.map((item, index) => ( <VideoPlayerDialogPlaylistItem key={item.id} active={index === activeIndex} index={index + 1} poster={item.poster} title={item.title} duration={item.duration} onClick={() => setActiveIndex(index)} /> ))} </VideoPlayerDialogPlaylistList> </VideoPlayerDialogPlaylist> ) : null} </VideoPlayerDialogContent> </VideoPlayerDialog> );}See the dedicated App Video Player Dialog recipe for usage, ownership, and customization guidance.
<AppVideoPlayerDialog
trigger={<Button>Watch video</Button>}
title="A guide to buying your first home"
description="What to prepare before beginning your home-buying journey."
src="/videos/first-home.mp4"
poster="/images/first-home.jpg"
/>Sizes
VideoPlayerDialogContent accepts sm, md, and lg, with maximum widths of 584px, 752px, and 920px. Every size remains viewport-safe. When content reaches the maximum height, the whole rounded surface becomes one scroll region.
Playlist
Playlist primitives provide layout and active-item semantics without owning selection state. The indicator communicates current position and total, such as 2 / 5. The local wrapper decides whether completing a video stops or advances to the next item.
The indicator is also the way down to the playlist: clicking it scrolls the dialog's surface to the playlist section, since on a tall video the count is the only sign a playlist is there at all. It finds the playlist within its own dialog surface, so a second dialog on the page is never the one that moves, and it respects prefers-reduced-motion by jumping rather than animating. A dialog that reveals its playlist some other way opts out by calling preventDefault in its own onClick.
Primitive API
| Primitive | Responsibility |
|---|---|
VideoPlayerDialog | Accessible modal root; accepts the underlying dialog root props. |
VideoPlayerDialogTrigger | Opens the dialog using Base UI's render composition. |
VideoPlayerDialogContent | Portal, backdrop, close button, responsive width, centering, theme, and bounded scrolling surface. |
VideoPlayerDialogMedia | Behavior-free 16:9 slot for any MediaPlayer composition. |
VideoPlayerDialogDetails | Size-aware metadata spacing. |
VideoPlayerDialogTitle | Accessible dialog title with size-aware typography. |
VideoPlayerDialogDescription | Accessible dialog description with size-aware typography. |
VideoPlayerDialogPlaylistIndicator | Glass current/total button positioned over media. Scrolls to the playlist on click. |
VideoPlayerDialogPlaylist | Playlist section and separator. |
VideoPlayerDialogPlaylistHeader | Playlist heading. |
VideoPlayerDialogPlaylistList | Semantic list container. |
VideoPlayerDialogPlaylistItem | Selectable item with active semantics, index, poster, title, and duration slots. |
Ownership
The design-system primitives own:
- Modal accessibility, focus containment, and dismissal
- Responsive dimensions, maximum height, scrolling, and surface styling
- Media, metadata, indicator, and playlist layout slots
- Active playlist item semantics
The application's local wrapper owns:
- Media URLs and API response mapping
- Active playlist state and automatic-next policy
- Analytics, authorization, and product copy
- The exact set and arrangement of
MediaPlayercontrols
Accessibility
- Always include
VideoPlayerDialogTitle, or provide an explicit accessible label onVideoPlayerDialogContent. - Provide captions with
MediaPlayerVideochildren when video contains speech. - Set
activeon the selected playlist item so it exposesaria-current. - The playlist indicator is a button named "Video 2 of 5. Go to the playlist." — the visible count is decorative, so the name has to carry both the position and the destination.
- Do not autoplay initial playback with sound. If a local wrapper advances a user-started playlist, stop after the final item.