A Mantine-native video player for React that ships as three layers in one package: a polished default
<Video />, a composable compound API for the control bar, and a fully headlessuseVideohook.
Introduction
Imagine you're building a landing page. The hero needs a looping background video. The product section needs an inline player with a clean control bar. And somewhere on the page, there's a feature demo where you want a completely custom UI — circular play button, vertical timeline, custom seek preview. With the native HTML <video> element, that's three different implementations, each with its own quirks and audio glitches. With @gfazioli/mantine-video, it's one component, three patterns, zero compromises.
What is mantine-video?
@gfazioli/mantine-video is a video player for React applications built with Mantine 9 and React 19. It wraps the native HTML <video> element with three layers of abstraction, layered from "drop-in" to "fully custom":
-
<Video />— a fully themed, batteries-included player that works out of the box. -
A compound API (
Video.Controls,Video.PlayButton,Video.Timeline, …) for composable control bars. -
useVideoheadless hook — full state and actions for building a 100% custom UI on top of a plain<video>element.
Every part is theme-aware (color scheme, Mantine theme colors, radii, sizes), accessible (ARIA labels, keyboard shortcuts, focus management), and customizable through the full Mantine Styles API. Four variants — overlay, minimal, floating, bordered — cover the most common layouts out of the box. Picture-in-Picture, fullscreen, captions and live timeline scrubbing are wired in by default.
Oh, and the same player can also be a background video. More on that below.
✨ Key Features
A polished default player
The fastest path: pass src, optionally poster and aspectRatio, and you have a complete player with play/pause, scrubbable timeline, time display, volume control, captions toggle, Picture-in-Picture and fullscreen.
import { Video } from '@gfazioli/mantine-video';
function Demo() {
return (
<Video
src="https://example.com/video.mp4"
poster="https://example.com/poster.jpg"
aspectRatio={16 / 9}
/>
);
}
Compound API for custom control bars
The default control bar is just a sensible composition of compound sub-components. Pass controls={false} and bring your own children to reorder, remove, or add controls — without losing Mantine theming or accessibility.
<Video src="..." aspectRatio={16 / 9} controls={false}>
<Video.Controls>
<Video.PlayButton />
<Video.SkipButton seconds={-10} />
<Video.SkipButton seconds={10} />
<Video.Timeline />
<Video.TimeDisplay format="current/-remaining" />
<Video.MuteButton />
<Video.CaptionsButton />
<Video.PiPButton />
<Video.FullscreenButton />
</Video.Controls>
</Video>
Nine compound sub-components available: Video.Controls, Video.PlayButton, Video.SkipButton, Video.Timeline, Video.TimeDisplay, Video.MuteButton, Video.CaptionsButton, Video.PiPButton, Video.FullscreenButton.
Headless useVideo hook
When the compound API isn't flexible enough, skip the <Video /> component entirely and use the useVideo hook with a plain <video> element. You get full state plus a complete set of actions.
import { ActionIcon, Slider } from '@mantine/core';
import { IconPlayerPauseFilled, IconPlayerPlayFilled } from '@tabler/icons-react';
import { useVideo } from '@gfazioli/mantine-video';
function CustomPlayer() {
const video = useVideo();
return (
<div>
<video ref={video.videoRef} src="..." />
<ActionIcon onClick={video.toggle}>
{video.playing ? <IconPlayerPauseFilled /> : <IconPlayerPlayFilled />}
</ActionIcon>
<Slider
value={video.currentTime}
max={video.duration}
onChange={video.seek}
/>
</div>
);
}
The hook exposes 16 state values (playing, paused, ended, currentTime, duration, buffered, volume, muted, playbackRate, fullscreen, pip, isLoading, error, canPlay, canFullscreen, canPiP) and 16 actions (play, pause, toggle, seek, seekBy, setVolume, mute, unmute, toggleMute, setPlaybackRate, requestFullscreen, exitFullscreen, toggleFullscreen, requestPiP, exitPiP, togglePiP) plus the two refs (videoRef, containerRef).
Live timeline scrubbing
Drag the timeline thumb — the underlying <video> seeks in real time and you see the frame update under your finger, YouTube-style. The player pauses automatically during the drag and resumes on release if it was playing. Throttled with requestAnimationFrame so it stays smooth even on heavier videos. Opt out with <Video.Timeline liveScrub={false} /> to fall back to commit-on-release.
Four built-in variants
<Video src="..." variant="overlay" /> {/* default — YouTube-style overlay */}
<Video src="..." variant="minimal" /> {/* controls below the video, page flow */}
<Video src="..." variant="floating" /> {/* controls in a glass-morphism card */}
<Video src="..." variant="bordered" /> {/* bordered frame for cards/lists */}
Each variant changes only the position and styling of the controls — the API and sub-components stay identical.
Background video — one prop turns the player into a hero
This is where the layered API really pays off. Set asBackground and the same player becomes an absolute-positioned, cover-cropped element ready to drop inside any hero section.
<Box pos="relative" h="100vh">
<Video src="..." asBackground autoPlay muted loop />
<Title>Welcome to my product</Title>
{/* Your hero content overlaid on top */}
</Box>
When asBackground is on, the component:
- Positions itself absolutely (
position: absolute; inset: 0) inside its parent - Applies
object-fit: coveron the underlying<video> - Disables
controls,clickToToggle,shortcutsandautoHideControlsas defaults (you can still re-enable any of them explicitly) - Renders a discreet floating mute toggle in the bottom-right corner, controllable via the
backgroundMuteButtonprop
For finer control, the standalone fit prop accepts 'cover' | 'contain' | 'fill' | 'none' | 'scale-down' and maps directly to CSS object-fit.
Picture-in-Picture
PiP is wired in by default. The browser's native always-on-top window can be launched via:
- The
<Video.PiPButton />— auto-hides on browsers without the standard API (notably Firefox) - The P keyboard shortcut
- Programmatically via
useVideo().togglePiP()
Plus two lifecycle callbacks to react to the user popping the video out:
<Video
src="..."
onEnterPictureInPicture={() => console.log('PiP open')}
onLeavePictureInPicture={() => console.log('PiP closed')}
/>
Keyboard shortcuts
When shortcuts is enabled (default) and the player has focus:
- Space / K — toggle play / pause
- J / L — seek -10s / +10s
- ← / → — seek -5s / +5s
- ↑ / ↓ — volume up / down
- M — mute toggle
- F — fullscreen toggle
- P — Picture-in-Picture toggle
🚀 Getting Started
npm install @gfazioli/mantine-video
# or
yarn add @gfazioli/mantine-video
Import the styles at the root of your application:
import '@gfazioli/mantine-video/styles.css';
A layered version is also available under @layer mantine-video for fine-grained cascade control:
import '@gfazioli/mantine-video/styles.layer.css';
Then drop it anywhere:
import { Video } from '@gfazioli/mantine-video';
export function Demo() {
return (
<Video
src="https://example.com/video.mp4"
poster="https://example.com/poster.jpg"
aspectRatio={16 / 9}
/>
);
}
Props & API
The <Video /> component accepts a rich set of props grouped by responsibility:
Source & playback
-
src: string— video source URL -
poster: string— image displayed before playback starts -
autoPlay,muted,loop,playsInline,preload— forwarded to the underlying<video> -
playing/currentTime/volume/playbackRate— controlled props with matchingonPlayChange/onCurrentTimeChange/onVolumeChange/onPlaybackRateChangecallbacks
Look & feel
variant: 'overlay' | 'minimal' | 'floating' | 'bordered'color: MantineColorradius: MantineRadiussize: MantineSize-
aspectRatio: number— e.g.16 / 9
Interactivity
-
controls: boolean— render the default control bar (defaulttrue) -
clickToToggle: boolean— click anywhere on the video to play / pause doubleClickToFullscreen: boolean-
shortcuts: boolean— enable keyboard shortcuts when focused -
autoHideControls: number— ms of inactivity before controls fade out (0disables)
Background mode
-
asBackground: boolean— preset for hero / section backgrounds -
backgroundMuteButton: boolean— floating mute toggle whenasBackgroundis on (defaulttrue) -
fit: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'— CSSobject-fiton the<video>
Lifecycle callbacks
-
onEnded()— playback reached the end of the media onError(error: MediaError | null)-
onEnterPictureInPicture()/onLeavePictureInPicture() onFullscreenChange(fullscreen: boolean)
🎨 Styling
<Video /> supports the full Mantine Styles API. Every sub-part is targetable through classNames, styles, vars and unstyled props.
Selectors: root, video, controls, controlBar, playButton, timeline, timelineBuffered, timeDisplay, muteButton, fullscreenButton, pipButton, captionsButton, skipButton, iconButton, backgroundMuteButton.
CSS variables: --video-color, --video-radius, --video-bg, --video-controls-bg, --video-controls-height, --video-controls-text-color, --video-timeline-color, --video-timeline-thumb-color, --video-object-fit.
<Video
src="..."
classNames={{ playButton: 'my-play-button' }}
styles={{ controls: { background: 'transparent' } }}
vars={() => ({
root: { '--video-color': 'tomato' } as Record<string, string>,
})}
/>
Live Demo & Documentation
The docs site at gfazioli.github.io/mantine-video ships with ten interactive demos covering every API surface — an interactive configurator, the basic player, custom controls, each of the four variants, headless usage via useVideo, Picture-in-Picture lifecycle, and the Styles API playground.
There are also two full-bleed standalone pages that show the asBackground mode in real landing-page scenarios:
-
/fullscreen— a single immersive hero with title + CTA over a looping video -
/homepage— a multi-section landing page that alternates three different background videos with text content and a features grid

Top comments (0)