Smarter Mobile Video Controls
Building a strong mobile media experience starts with a simple question: how important is the video to the page? If the video is the main reason someone is visiting, the playback experience needs to feel immersive and easy to re-engage with. Several modern Web APIs let you enhance a basic player progressively, adding custom controls, seamless fullscreen behavior, and background playback handling without locking users into a single platform's defaults.
Building Custom, Event-Driven Controls
The foundation is a standard <video> element wrapped in a container <div>, with a separate child <div> for the controls. The custom control set includes play/pause and fullscreen buttons, seek backward and forward buttons, plus current time, duration, and a progress bar.
Start by waiting for the video metadata to load before setting the duration, current time, and initializing the progress bar. A utility function like secondsToTimeCode() converts the raw seconds value into a more readable "hh:mm:ss" format.
For play and pause, attach the click handler to your button, calling video.play() or video.pause() based on current state. The key is syncing the UI with the play and pause events rather than only in the click listener. This event-based pattern keeps the controls flexible (useful later with the Media Session API) and ensures they stay correct if the browser itself changes playback state. When playback starts, switch the button label to "pause" and hide the controls; when it pauses, switch it back and show them again.
The timeupdate event fires as the currentTime attribute changes, which lets you update the visible controls and progress bar. When the video ends, reset the state to "play," set currentTime back to 0, and show the controls again. (This is also the moment you might load another video if an autoplay-next feature is enabled.)
Seek backward and forward buttons let users skip content easily. Use the seeking and seeked events, not the button click itself, to adjust the video's visual state — for instance, by applying a CSS class that dims the video with filter: brightness(0); while it searches for the new position.
Creating a Truly Fullscreen Experience
Native mobile video players often force their own fullscreen behavior. To create a seamless, custom flow, you may need to use several APIs together. You don't need all of them — pick the pieces that fit your needs.
Stopping Automatic Fullscreen
On iOS, videos enter fullscreen automatically when playback starts. To keep control over the experience across mobile browsers, set the playsinline attribute on the <video> element. It forces inline playback on iPhone without side effects on other browsers.
Manual Fullscreen Toggle
With automatic behavior prevented, handle fullscreen yourself with the Fullscreen API. In the button's click handler, check the current state: if the document is already in fullscreen, call document.exitFullscreen(). Otherwise, request fullscreen on the video's container element via requestFullscreen(). On iOS, where that API isn't fully supported, fall back to webkitEnterFullscreen() on the video element itself.
Reacting to Device Orientation
A good mobile experience jumps to fullscreen when the user rotates the device to landscape. Listen for changes on the Screen Orientation API, which may still be prefixed in some browsers — this is a chance for progressive enhancement. When the screen orientation changes and the window is landscape (width greater than height), request fullscreen; if it's portrait, exit fullscreen.
Conversely, the "fullscreen button" can initiate the lock: using screen.orientation.lock('landscape') locks the screen. Do this only when the device is portrait (matchMedia('(orientation: portrait)')) and is small enough to hold in one hand (matchMedia('(max-device-width: 768px)')) — locking landscape on a tablet is a poor experience.
The lock creates a puzzle: once the screen is locked, your listener for portrait orientation changes won't fire. The Device Orientation API solves that. It reports physical rotation from the device's hardware, regardless of the locked screen state. When the device orientation reports portrait while the screen is locked to landscape, call screen.orientation.unlock() to let the interface rotate naturally. Combine these to get a fullscreen flow that feels like it's always a step ahead of the user's hands.
Handling Playback Beyond the Viewport
Video playback doesn't stop being your responsibility when the tab is hidden or the player scrolls out of view.
Pausing When the Page is Hidden
The Page Visibility API tells you when a page is invisible — when the screen locks or the user switches tabs, for example. Hook into this to pause the video. Since many mobile browsers provide outside controls to resume playback from a lock screen, it's sensible to pause only if you aren't intentionally supporting background playback.
Checking Visibility with Intersection Observer
The Intersection Observer API gives you more granular information: it notifies you when the player element enters or leaves the viewport. A practical pattern is showing a small mute button in the bottom-right corner if a video is playing but has scrolled out of view. Use the volumechange event to properly style that mini control's state.
If your page embeds multiple videos, enforce playback of only one at a time. You can add listeners to pause the other players whenever a new one attempts to play, preventing cacophonous overlapping audio.
Customizing the Media Notification
When your app plays audio or video, the browser shows a media notification in the tray. Without your input, Chrome does its best, using the document title and whatever icon it finds. The Media Session API puts you in charge of that space.
Set metadata like the track's title, artist, album, and artwork directly on navigator.mediaSession.metadata:
playPauseButton.addEventListener('click', function(event) {
event.stopPropagation();
if (video.paused) {
video.play()
<strong>.then(function() {
setMediaSession();
});</strong>
} else {
video.pause();
}
});
If the session's playbackState is supported, update it as well. You don't need to "release" the session when playback ends — the notification disappears automatically. Remember that whatever you assign to navigator.mediaSession.metadata will be used at the start of any subsequent playback, so update it as the playing content changes to keep the notification relevant.
Adding Media Actions
For playlists, the notification can include "Previous Track" and "Next Track" actions. Set a handler via navigator.mediaSession.setActionHandler() to let users navigate from the tray. These action handlers act much like event listeners but stop the browser's own defaults. Consequently, controls won't appear in the notification unless an explicit action handler is set for them — for example, the "Seek Backward" and "Seek Forward" icons only show after you define the corresponding seekbackward and seekforward handlers. To remove a handler, set it to null.
Play and pause icons are always visible in the notification, with their events handled by the browser automatically. If the default handling doesn't fit your scenario, you can intercept those events with the Media Session API as well.
The Media Session API's real benefit is that this metadata and those controls aren't just in the notification tray. They sync automatically to the lock screen and any paired wearable device, making the media experience truly omnipresent without any platform-specific code.



