Mantine Video

Logo

@gfazioli/mantine-video

A customizable video player component for React built with Mantine. Compound API, headless useVideo hook, theme integration, and full Styles API support.

Installation

yarn add @gfazioli/mantine-video

After installation import package styles at the root of your application:

import '@gfazioli/mantine-video/styles.css';

You can import styles within a layer @layer mantine-video by importing @gfazioli/mantine-video/styles.layer.css file.

import '@gfazioli/mantine-video/styles.layer.css';

Usage

A video player built on the native HTML <video> element, themed with Mantine and composed of accessible building blocks. The default <Video /> renders a control bar with play / pause, timeline, time display, volume and fullscreen. Use the configurator below to explore the props in real time.

0:00 / 0:00

Color
Radius
Aspect ratio
Auto hide controls
import { Video } from '@gfazioli/mantine-video';

function Demo() {
  return (
    <Video
      src="/videos/manta.mp4"
      poster="/videos/manta-poster.jpg"
      aspectRatio={{{aspectRatio}}}
      variant="{{variant}}"
      color="{{color}}"
      radius="{{radius}}"
      autoHideControls={{{autoHideControls}}}
      shortcuts={{{shortcuts}}}
      clickToToggle={{{clickToToggle}}}
      controls={{{controls}}}
    />
  );
}

Basic example

The simplest usage: pass src, optionally poster and aspectRatio and you get a fully themed player out of the box. The control bar fades in on hover and auto-hides after a few seconds while playing.

0:00 / 0:00

import { Video } from '@gfazioli/mantine-video';

function Demo() {
  return (
    <Video
      src="/videos/manta.mp4"
      poster="/videos/manta-poster.jpg"
      aspectRatio={16 / 9}
    />
  );
}

Custom controls

The control bar is a compound component made of <Video.PlayButton />, <Video.Timeline />, <Video.TimeDisplay />, <Video.MuteButton />, <Video.SkipButton />, <Video.CaptionsButton />, <Video.PiPButton /> and <Video.FullscreenButton />. Pass controls={false} to opt out of the default layout and compose your own.

0:00 / -0:00

import { Video } from '@gfazioli/mantine-video';

function Demo() {
  return (
    <Video
      src="/videos/manta.mp4"
      poster="/videos/manta-poster.jpg"
      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>
  );
}

Variants

The component ships with four built-in variants. Each variant changes where and how the controls render relative to the video frame, but the API and sub-components stay identical — only variant changes.

overlay (default)

Controls float on top of the video at the bottom, with a soft dark gradient backdrop. They fade in on hover and auto-hide after a few seconds while playing, just like a typical YouTube-style player. This is the right default when the player is the focal point of the page.

0:00 / 0:00

import { Video } from '@gfazioli/mantine-video';

function Demo() {
  return (
    <Video
      src="/videos/manta.mp4"
      poster="/videos/manta-poster.jpg"
      aspectRatio={16 / 9}
      variant="overlay"
    />
  );
}

minimal

The controls are placed below the video frame in the regular page flow. No overlay, no auto-hide, no gradient. The controls use the standard Mantine surface colors so they integrate naturally with the rest of the UI. Useful when the video is one element among many (a list, a card grid, a chat thread) and you want the controls to feel like part of the surrounding UI rather than a player chrome.

0:00 / 0:00

import { Video } from '@gfazioli/mantine-video';

function Demo() {
  return (
    <Video
      src="/videos/manta.mp4"
      poster="/videos/manta-poster.jpg"
      aspectRatio={16 / 9}
      variant="minimal"
    />
  );
}

floating

Same overlay behavior as overlay, but the control bar is rendered inside a rounded floating card with a blurred backdrop, slightly inset from the video edges. A bit more "premium" / glass-morphism feel, good for hero videos and media-rich landing pages.

0:00 / 0:00

import { Video } from '@gfazioli/mantine-video';

function Demo() {
  return (
    <Video
      src="/videos/manta.mp4"
      poster="/videos/manta-poster.jpg"
      aspectRatio={16 / 9}
      variant="floating"
    />
  );
}

bordered

A hybrid: the container has a border and the controls are placed below the video (like minimal), but the whole player — frame + controls — is wrapped in a single bordered container. Best for embedding inside cards or wrapping a list of media items where you want a clearly delimited boundary.

0:00 / 0:00

import { Video } from '@gfazioli/mantine-video';

function Demo() {
  return (
    <Video
      src="/videos/manta.mp4"
      poster="/videos/manta-poster.jpg"
      aspectRatio={16 / 9}
      variant="bordered"
    />
  );
}

Headless usage with useVideo

When the compound API is not flexible enough, use the useVideo hook to manage state and build a fully custom UI. The hook returns the video state and a complete set of actions and refs.

0.0s / 0.0s

import { ActionIcon, Group, Slider, Text } from '@mantine/core';
import { IconPlayerPauseFilled, IconPlayerPlayFilled } from '@tabler/icons-react';
import { useVideo } from '@gfazioli/mantine-video';

function Demo() {
  const video = useVideo();
  return (
    <div>
      <video
        ref={video.videoRef}
        src="/videos/manta.mp4"
        style={{ width: '100%', aspectRatio: '16 / 9', background: 'black' }}
      />

      <Group mt="sm" gap="md">
        <ActionIcon onClick={video.toggle} variant="filled" size="lg">
          {video.playing ? <IconPlayerPauseFilled size={20} /> : <IconPlayerPlayFilled size={20} />}
        </ActionIcon>

        <Slider
          flex={1}
          value={video.currentTime}
          max={video.duration || 0}
          step={0.01}
          onChange={video.seek}
        />

        <Text size="sm" ff="monospace" miw={80} ta="right">
          {video.currentTime.toFixed(1)}s / {video.duration.toFixed(1)}s
        </Text>
      </Group>
    </div>
  );
}
const {
  videoRef, containerRef,
  playing, paused, ended, currentTime, duration, buffered,
  volume, muted, playbackRate, fullscreen, pip, isLoading, error,
  canPlay, canFullscreen, canPiP,
  play, pause, toggle, seek, seekBy, setVolume, mute, unmute, toggleMute,
  setPlaybackRate, requestFullscreen, exitFullscreen, toggleFullscreen,
  requestPiP, exitPiP, togglePiP,
} = useVideo();

Background video

The component can also be used as a section or full-page background — the kind of immersive hero you see on modern landing pages. The asBackground prop turns the player into an absolute-positioned, cover-cropped element ready to drop inside any positioned parent.

<Box pos="relative" h="100vh">
  <Video src="..." asBackground autoPlay muted loop />
  {/* Your hero content overlaid on top */}
</Box>

When asBackground is true the component:

  • positions itself absolutely (position: absolute; inset: 0) inside its parent
  • applies object-fit: cover to the underlying <video> element
  • disables controls, clickToToggle, shortcuts, doubleClickToFullscreen and autoHideControls as defaults (you can still re-enable any of them explicitly)
  • renders a discreet floating mute button in the bottom-right corner, controllable via the backgroundMuteButton prop (default true)

For finer control, the standalone fit prop accepts 'cover' | 'contain' | 'fill' | 'none' | 'scale-down' and maps directly to CSS object-fit on the <video> element.

See it live

Open the fullscreen demo pageOpen the homepage-style demo page. Both pages live outside the docs layout, full‑bleed.

Picture-in-Picture

The component supports the browser's native Picture-in-Picture API. When supported, the user can pop the video out into a floating, always-on-top window that survives tab switches and stays visible while they work in other apps.

How it works

Three things are wired up out of the box:

  1. <Video.PiPButton /> — an icon button in the default control bar. It calls requestPictureInPicture() / exitPictureInPicture() under the hood and self-hides if the browser does not expose the standard API (notably Firefox, which still uses its own native button).
  2. Keyboard shortcut — pressing P while the player has focus (and shortcuts is enabled) toggles PiP.
  3. useVideo hook — exposes pip state, canPiP capability and requestPiP / exitPiP / togglePiP actions so you can drive PiP from any UI.

Lifecycle callbacks

The Video component accepts onEnterPictureInPicture and onLeavePictureInPicture callbacks. Use them to react to the user popping the video in and out — for example to dim a sidebar, change the page layout or show a "playing in PiP" indicator.

0:00 / 0:00

import { useState } from 'react';
import { Alert, Stack, Text } from '@mantine/core';
import { IconPictureInPicture, IconPictureInPictureOn } from '@tabler/icons-react';
import { Video } from '@gfazioli/mantine-video';

function Demo() {
  const [pipActive, setPipActive] = useState(false);

  return (
    <Stack gap="md">
      <Alert
        color={pipActive ? 'green' : 'blue'}
        variant="light"
        icon={pipActive ? <IconPictureInPictureOn /> : <IconPictureInPicture />}
        title={pipActive ? 'Picture-in-Picture is ACTIVE' : 'Picture-in-Picture is inactive'}
      >
        <Text size="sm">
          Click the PiP button on the control bar (right before Fullscreen) or press the P key
          while the player has focus.
        </Text>
      </Alert>

      <Video
        src="/videos/manta.mp4"
        poster="/videos/manta-poster.jpg"
        aspectRatio={16 / 9}
        onEnterPictureInPicture={() => setPipActive(true)}
        onLeavePictureInPicture={() => setPipActive(false)}
      />
    </Stack>
  );
}

Browser support

BrowserSupport
Chrome / Edge (desktop & Android)Full standard API
Safari macOS 14+Full standard API
Safari iOS 14+Standard API, requires playsInline (default true)
FirefoxNo standard API — uses its own button in the native controls. Video.PiPButton is hidden automatically.

Limits of the underlying API

  • Must be triggered by a user gesture (click, tap, key press). Programmatic auto-PiP is blocked by the browser.
  • Only one PiP video at a time per browser. Entering PiP on another video closes the previous one.
  • PiP window size is controlled by the user — the page cannot resize it.
  • Videos with DRM-protected content may opt out of PiP.

Keyboard shortcuts

When shortcuts is enabled (default) and the player has focus:

KeyAction
Space / KToggle play / pause
Arrow LeftSeek -5 s
Arrow RightSeek +5 s
JSeek -10 s
LSeek +10 s
Arrow Up / DownVolume + / -
MToggle mute
FToggle fullscreen
PToggle Picture-in-Picture

Styles API

Video supports the full Mantine Styles API. Use classNames, styles, vars and unstyled to customize the root container, the underlying <video> element and every sub-component (controls, controlBar, playButton, timeline, timelineBuffered, timeDisplay, muteButton, fullscreenButton, pipButton, captionsButton, skipButton). Use the interactive playground below to inspect every selector and CSS variable.

0:00 / 0:00

Component Styles API

Hover over selectors to highlight corresponding elements

/*
 * Hover over selectors to apply outline styles
 *
 */