DEV Community

Cover image for The Player, the Pieces, and the Hook
Giovambattista Fazioli
Giovambattista Fazioli

Posted on

The Player, the Pieces, and the Hook

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 headless useVideo hook.

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.

Mantine Video

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":

  1. <Video /> — a fully themed, batteries-included player that works out of the box.
  2. A compound API (Video.Controls, Video.PlayButton, Video.Timeline, …) for composable control bars.
  3. useVideo headless 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}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 */}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

When asBackground is on, the component:

  • Positions itself absolutely (position: absolute; inset: 0) inside its parent
  • Applies object-fit: cover on the underlying <video>
  • Disables controls, clickToToggle, shortcuts and autoHideControls as defaults (you can still re-enable any of them explicitly)
  • Renders a discreet floating mute toggle in the bottom-right corner, controllable via the backgroundMuteButton prop

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')}
/>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Import the styles at the root of your application:

import '@gfazioli/mantine-video/styles.css';
Enter fullscreen mode Exit fullscreen mode

A layered version is also available under @layer mantine-video for fine-grained cascade control:

import '@gfazioli/mantine-video/styles.layer.css';
Enter fullscreen mode Exit fullscreen mode

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}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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 matching onPlayChange / onCurrentTimeChange / onVolumeChange / onPlaybackRateChange callbacks

Look & feel

  • variant: 'overlay' | 'minimal' | 'floating' | 'bordered'
  • color: MantineColor
  • radius: MantineRadius
  • size: MantineSize
  • aspectRatio: number — e.g. 16 / 9

Interactivity

  • controls: boolean — render the default control bar (default true)
  • 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 (0 disables)

Background mode

  • asBackground: boolean — preset for hero / section backgrounds
  • backgroundMuteButton: boolean — floating mute toggle when asBackground is on (default true)
  • fit: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down' — CSS object-fit on 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>,
  })}
/>
Enter fullscreen mode Exit fullscreen mode

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

Links

Top comments (0)