utomatically. It ships with four layout variants that adjust control positioning and styling without altering the underlying API.
import { Video } from '@gfazioli/mantine-video';
function StandardPlayer() {
return (
<Video
source="https://cdn.example.com/lecture-01.mp4"
thumbnail="https://cdn.example.com/lecture-01-thumb.jpg"
ratio={16 / 9}
layout="overlay"
autoHideDelay={3000}
/>
);
}
Architecture Rationale: The default layer abstracts event binding, keyboard shortcuts, and control rendering. It enforces browser autoplay policies by defaulting to muted playback when autoPlay is enabled. The layout prop switches between overlay, minimal, floating, and bordered configurations, each adjusting CSS positioning and control bar placement while preserving identical sub-component APIs.
Layer 2: Compound Control API
When the default control bar doesn't match the product design, the compound pattern allows granular composition. Each control primitive inherits Mantine theme tokens and accessibility attributes, eliminating the need to manually wire ARIA labels or focus management.
import { Video } from '@gfazioli/mantine-video';
import { IconRewind, IconFastForward } from '@tabler/icons-react';
function CustomControlBar() {
return (
<Video source="https://cdn.example.com/demo.mp4" ratio={16 / 9} hideDefaultControls>
<Video.ControlGroup>
<Video.PlayToggle />
<Video.SeekButton offset={-15} icon={<IconRewind />} />
<Video.SeekButton offset={15} icon={<IconFastForward />} />
<Video.ProgressTrack liveScrub />
<Video.TimeIndicator mode="elapsed/total" />
<Video.VolumeControl />
<Video.CaptionToggle />
<Video.PictureInPictureToggle />
<Video.FullscreenToggle />
</Video.ControlGroup>
</Video>
);
}
Architecture Rationale: The compound pattern separates layout composition from state management. hideDefaultControls disables the built-in bar while preserving the media engine. Sub-components like ProgressTrack with liveScrub use requestAnimationFrame throttling to update the underlying video element during drag operations, automatically pausing playback mid-drag and resuming on release. This prevents jank on high-resolution media while maintaining YouTube-style scrubbing responsiveness.
Layer 3: Headless State Hook
For completely bespoke interfaces, the useVideo hook decouples media state from DOM rendering. It exposes 16 reactive state properties and 16 imperative actions, plus two refs for direct element attachment.
import { useRef, useCallback } from 'react';
import { useVideo } from '@gfazioli/mantine-video';
import { ActionIcon, Slider, Group, Text } from '@mantine/core';
import { IconPlayerPlay, IconPlayerPause } from '@tabler/icons-react';
function HeadlessMediaController() {
const mediaEngine = useVideo();
const containerRef = useRef<HTMLDivElement>(null);
const handleRateChange = useCallback((rate: number) => {
mediaEngine.setPlaybackRate(rate);
}, [mediaEngine]);
return (
<div ref={containerRef} style={{ position: 'relative' }}>
<video
ref={mediaEngine.videoRef}
src="https://cdn.example.com/interactive.mp4"
style={{ width: '100%', borderRadius: '8px' }}
/>
<Group position="apart" mt="md">
<ActionIcon onClick={mediaEngine.toggle} variant="light">
{mediaEngine.playing ? <IconPlayerPause /> : <IconPlayerPlay />}
</ActionIcon>
<Slider
flex={1}
value={mediaEngine.currentTime}
max={mediaEngine.duration}
onChange={mediaEngine.seek}
label={`${Math.floor(mediaEngine.currentTime)}s`}
/>
<Text size="sm" fw={500}>
{mediaEngine.playbackRate}x
</Text>
</Group>
</div>
);
}
Architecture Rationale: The headless layer is framework-agnostic in its UI output but tightly coupled to React's rendering cycle for state synchronization. It manages event listeners internally, preventing memory leaks common in manual useEffect implementations. The hook exposes canPlay, canFullscreen, and canPiP flags to conditionally render controls based on browser capability detection, eliminating runtime errors on unsupported environments like Firefox (which lacks native PiP support).
Background Mode Architecture
The asBackground prop transforms the player into a layout-aware hero element. It applies absolute positioning, object-fit: cover, and disables interactive defaults (controls, clickToToggle, shortcuts) to prevent accidental playback triggers. A floating mute toggle remains available via backgroundMuteButton to comply with browser autoplay policies while allowing user-initiated audio.
import { Box, Title, Video } from '@gfazioli/mantine-video';
function HeroSection() {
return (
<Box style={{ position: 'relative', height: '100vh', overflow: 'hidden' }}>
<Video
source="https://cdn.example.com/hero-loop.mp4"
asBackground
autoPlay
muted
loop
fit="cover"
/>
<Title order={1} style={{ position: 'relative', zIndex: 1, color: 'white' }}>
Next-Generation Analytics
</Title>
</Box>
);
}
Pitfall Guide
1. Autoplay Policy Violations
Explanation: Modern browsers block unmuted autoplay. Attempting to play audio immediately without user interaction throws a NotAllowedError and breaks playback state.
Fix: Always pair autoPlay with muted. Use the backgroundMuteButton or explicit unmute controls to transition to audio playback after user gesture.
2. Ref Collision & Memory Leaks
Explanation: Manually attaching useRef to the <video> element while also using the library's hook causes dual event binding, resulting in duplicate listeners and state desynchronization.
Fix: Rely exclusively on mediaEngine.videoRef and mediaEngine.containerRef. Never attach custom refs to the same DOM nodes.
3. CSS Variable Specificity Conflicts
Explanation: Overriding Mantine theme tokens via inline styles or global CSS can break the player's internal variable cascade (--video-color, --video-controls-bg, etc.), causing layout shifts or invisible controls.
Fix: Use the Mantine Styles API (classNames, styles, vars) or CSS layers (@layer mantine-video) to maintain cascade priority. Avoid !important overrides.
Explanation: Picture-in-Picture and Fullscreen APIs are not uniformly supported. Firefox lacks PiP, and some mobile browsers restrict fullscreen to user-initiated events.
Fix: Check canPiP and canFullscreen before rendering toggle buttons. Wrap API calls in try/catch blocks and provide fallback UI when capabilities are unavailable.
5. Unthrottled Timeline Updates
Explanation: Binding onTimeUpdate directly to React state without throttling causes excessive re-renders, leading to UI jank during playback or scrubbing.
Fix: Use the built-in ProgressTrack component, which implements requestAnimationFrame throttling. If building custom scrubbing, debounce state updates or use useRef for intermediate values.
6. Missing Keyboard Focus Management
Explanation: Custom control bars often forget to manage focus rings and tab order, breaking accessibility compliance and keyboard navigation shortcuts (Space/K, J/L, arrows, M, F, P).
Fix: Preserve the library's focus management by using compound components. If building headless, explicitly attach onKeyDown handlers and ensure tabIndex is correctly applied to interactive elements.
7. Layout Shifts from Aspect Ratio Mismatch
Explanation: Omitting aspectRatio or fit props causes the video container to collapse before media loads, triggering Cumulative Layout Shift (CLS) penalties.
Fix: Always specify aspectRatio or explicit container dimensions. Use fit="cover" for background modes and fit="contain" for tutorial content to prevent cropping artifacts.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Marketing hero with looping background | asBackground + overlay variant | Zero-config layout, autoplay-compliant, minimal bundle | Low (default implementation) |
| SaaS dashboard with standard playback | Default <Video /> + minimal variant | Built-in accessibility, theme-aware, fast delivery | Low (single component) |
| Custom tutorial platform with branded controls | Compound API (Video.ControlGroup) | Granular control composition, preserves theming/ARIA | Medium (layout customization) |
| Interactive data visualization with custom UI | useVideo headless hook | Complete DOM decoupling, framework-agnostic rendering | High (custom UI development) |
| Multi-tenant app with dynamic theming | Mantine Styles API + CSS variables | Runtime token injection without recompilation | Medium (theme engineering) |
Configuration Template
// app/providers/video-provider.tsx
import { MantineProvider, createTheme } from '@mantine/core';
import '@gfazioli/mantine-video/styles.layer.css';
const videoTheme = createTheme({
components: {
Video: {
defaultProps: {
radius: 'md',
size: 'md',
autoHideDelay: 2500,
shortcuts: true,
},
},
},
colors: {
brand: ['#1a1a2e', '#16213e', '#0f3460', '#e94560'],
},
});
export function VideoProvider({ children }: { children: React.ReactNode }) {
return <MantineProvider theme={videoTheme}>{children}</MantineProvider>;
}
Quick Start Guide
- Install the package: Run
npm install @gfazioli/mantine-video in your project root.
- Import styles: Add
import '@gfazioli/mantine-video/styles.css'; to your application entry point or use the layered variant for cascade control.
- Drop in the component: Replace your existing
<video> tags with <Video source="..." aspectRatio={16 / 9} /> and verify playback.
- Customize controls: Switch to compound components or
useVideo when the default UI doesn't match your design requirements.
- Validate accessibility: Test keyboard navigation, screen reader labels, and focus management across your target browsers before deployment.