Automatically show and hide controls
Show controls while the user is active, hide them during playback, and toggle them by touch.
Show controls while the user is active, fade them out during undisturbed playback, and bring them back on interaction.
Recommended approach
Wrap your control UI in Controls. It reads controls visibility from player state, so the whole control bar shows and hides as one unit.
import { Container, Controls, createPlayer, PlayButton, Time } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
return (
<Player>
<Container className="media-container">
<Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoPlay muted playsInline loop />
<Controls.Root>
<Controls.Backdrop className="controls-backdrop" />
<Controls.Content className="media-controls">
<Controls.Group className="controls-group" aria-label="Playback controls">
<PlayButton
className="button"
render={(props, state) => <button {...props}>{state.paused ? 'Play' : 'Pause'}</button>}
/>
<Time.Value type="current" className="time" />
</Controls.Group>
</Controls.Content>
</Controls.Root>
</Container>
</Player>
);
}
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.media-controls {
position: absolute;
inset: 0;
display: flex;
align-items: flex-end;
padding: 12px;
pointer-events: none;
transition: opacity 0.25s;
}
.controls-backdrop {
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(to top, rgba(0, 0, 0, 0.45), transparent 45%);
opacity: 1;
transition: opacity 0.35s;
}
.controls-backdrop:not([data-visible]),
.media-controls:not([data-visible]) {
opacity: 0;
}
.controls-group {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
pointer-events: auto;
}
.time {
display: inline-flex;
gap: 4px;
align-items: center;
padding-block: 8px;
padding-inline: 16px;
font-size: 14px;
color: black;
background: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
.button {
padding-block: 8px;
padding-inline: 16px;
font-size: 14px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
<video-player class="video-player">
<media-container>
<video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoplay muted playsinline loop></video>
<media-controls>
<media-controls-backdrop class="controls-backdrop"></media-controls-backdrop>
<media-controls-content class="media-controls">
<media-controls-group class="controls-group" aria-label="Playback controls">
<media-play-button class="button media-play-button">
<span class="paused">Play</span>
<span class="playing">Pause</span>
</media-play-button>
<media-time class="time" type="current"></media-time>
</media-controls-group>
</media-controls-content>
</media-controls>
</media-container>
</video-player>
.video-player media-container {
position: relative;
display: block;
}
.video-player media-container video {
width: 100%;
}
.media-controls {
position: absolute;
inset: 0;
display: flex;
align-items: flex-end;
padding: 12px;
pointer-events: none;
transition: opacity 0.25s;
}
.controls-backdrop {
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(to top, rgba(0, 0, 0, 0.45), transparent 45%);
opacity: 1;
transition: opacity 0.35s;
}
.controls-backdrop:not([data-visible]),
.media-controls:not([data-visible]) {
opacity: 0;
}
.controls-group {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
pointer-events: auto;
}
.time {
display: inline-flex;
gap: 4px;
align-items: center;
padding-block: 8px;
padding-inline: 16px;
font-size: 14px;
color: black;
background: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
.button {
padding-block: 8px;
padding-inline: 16px;
font-size: 14px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
.button .paused,
.button .playing {
display: none;
}
.media-play-button[data-paused] .paused {
display: inline;
}
.media-play-button:not([data-paused]) .playing {
display: inline;
}
import '@videojs/html/video/player';
import '@videojs/html/ui/container';
import '@videojs/html/ui/controls';
import '@videojs/html/ui/play-button';
import '@videojs/html/ui/time';
How it works
The controls feature tracks activity on the player container:
userActiveistruewhile the user interacts — pointer movement, keyboard input, or focus — and flips tofalseafter two seconds of inactivity.controlsVisiblederives from activity and playback: controls stay visible while paused or while a remote playback session is connecting or connected, regardless of idle state.toggleControls()flips visibility, for wiring to custom triggers.
On touch devices, a tap on the media toggles the controls instead of relying on pointer movement, and mid-gesture movement doesn’t flicker visibility.
The feature needs the player container for activity tracking; without a container it stays inactive and warns in development.
Availability and constraints
- Activity tracking listens on the player container, so controls placed outside it don’t keep the player “active” — put control UI inside the container.
- Keyboard focus counts as activity: controls stay reachable for keyboard and assistive-technology users while focus is inside them.
- Touch and mouse behave differently by design — mouse movement shows controls, touch requires a tap. Test both.
- The idle delay is fixed at two seconds.
Common variations
Always-visible controls
Skip the visibility wiring: render your controls without consuming controlsVisible. State still tracks activity for anything else that needs it.
Custom show/hide styling
Controls hide by state, so you own the transition. Style the visible and hidden states in CSS — fade, slide, or scale — driven by the component’s rendered state rather than re-implementing the idle timer.
Troubleshooting
Controls never hide
The media is paused (controls intentionally stay visible), a remote playback session is active, an open menu or popover holds a controls lock, or something keeps triggering activity — for example a focused element inside the container receiving repeated events.
Controls don’t come back on touch
A tap on the media toggles controls. If a custom overlay covers the media and swallows pointer events, taps never reach the container; let pointer events pass through decorative overlays.
Controls flicker on Android while tapping
Use the skin gestures (tap with the toggleControls action) rather than adding your own tap handlers next to the built-in behavior; competing handlers invert each other. See Add keyboard shortcuts and gestures.