play() and pause()
Every HTMLMediaElement — <video> and
<audio> alike — exposes play() and
pause() methods. Calling them from JavaScript gives you
complete control over playback without relying on the browser's built-in
controls attribute.
play() returns a Promise
This is the detail that surprises most developers: media.play()
does not return undefined — it returns a
Promise that resolves when playback has actually started, or
rejects if the browser blocks it (e.g., an autoplay policy violation or the
user navigating away). Always handle the rejection to avoid uncaught promise
errors in the console.
Reflecting state with events, not polling
Rather than checking video.paused in a loop, listen for the
play and pause events that the element fires
whenever its state changes. This works whether the state change was triggered
by your JavaScript, the native controls, or an autoplay policy. The demo
below wires up a single toggle button that stays in sync through these events.
Press Play — the button label flips to "Pause" the moment playback starts,
driven by the play event, not by a flag you manage yourself.
Press again and the pause event resets it. The browser's own
logic decides the authoritative state; you just reflect it.
Reading and setting properties
The HTMLMediaElement interface exposes several readable and
writable properties that describe — and control — the media's current state.
| Property | Type | Readable | Writable | Notes |
|---|---|---|---|---|
duration |
number (seconds) | Yes | No | Available after loadedmetadata fires. NaN beforehand. |
currentTime |
number (seconds) | Yes | Yes | Setting it seeks to that position. Values are clamped to [0, duration]. |
paused |
boolean | Yes | No | true when not playing (including before first play). |
volume |
number 0–1 | Yes | Yes | Out-of-range values throw a DOMException. |
muted |
boolean | Yes | Yes | Independent of volume. Toggling muted is the standard un-mute pattern. |
playbackRate |
number | Yes | Yes | 1.0 = normal, 0.5 = half speed, 2.0 = double. Most browsers support roughly 0.5–4×. |
Tracking progress with timeupdate
The timeupdate event fires several times per second while the
media is playing. It is the standard hook for updating a custom seek bar or
time display. Read currentTime and duration inside
the handler:
Seeking by setting currentTime
Writing to currentTime causes an immediate seek — the same
mechanism used by a custom seek bar. The demo below uses two buttons that
jump backward and forward by 2 seconds, plus a live progress bar and readout
driven by timeupdate.
The progress bar and text readout update in real time as you play the video
or use the seek buttons. You can also scrub with the native controls and
watch the JS readout stay in sync — both are reading from the same source of
truth: the element's currentTime.
Tip: duration is NaN until the
browser has downloaded enough of the file to know the total length. Always
wait for the loadedmetadata event before reading or displaying
duration.
Media events
The media element fires a rich set of events that let you respond to every phase of the playback lifecycle. Here are the most useful ones:
| Event | Fires when… |
|---|---|
loadedmetadata |
The browser has parsed the file headers: duration, videoWidth, and videoHeight are now valid. |
play |
Playback has been requested and the element transitions from paused to playing. |
pause |
Playback is suspended (user paused, .pause() called, or tab hidden). |
timeupdate |
The playback position changed — fires roughly 4–66 times per second while playing. |
ended |
Playback reached the end of the media and there is no loop attribute. |
volumechange |
volume or muted was changed. |
seeking |
A seek operation has started (e.g., user scrubbing or currentTime was set). |
seeked |
The seek has completed and playback is ready at the new position. |
waiting |
Playback has stalled because the browser is waiting for more data (buffering). |
Listening for events with addEventListener
Use addEventListener rather than setting on*
properties directly — it lets you attach multiple independent handlers to
the same event without the later one overwriting the earlier one.
The live demo below logs events to a visible panel so you can see the sequence in action. Play, pause, scrub, and let it finish to observe the event order.
Event log:
Notice that timeupdate fires far more often than the others —
several times per second. That is why the log coalesces it into a single
counter line rather than flooding the output.
Fullscreen and Picture-in-Picture
Two browser APIs let you move a video element out of its normal document flow: the Fullscreen API expands an element to cover the entire screen, and the Picture-in-Picture API (PiP) floats a small video overlay that persists while the user switches tabs or applications. Both require a user gesture — a click, tap, or keypress — to work; calling them from an autonomous script will throw or be silently ignored.
Fullscreen API
Call element.requestFullscreen() on any element (not just video).
To exit, call document.exitFullscreen(). The
fullscreenchange event fires on document when the
state transitions. Feature-detection is straightforward because the property
is present on all modern browsers without a prefix.
Picture-in-Picture API
PiP is video-specific. Feature-detect with two guards before calling:
'requestPictureInPicture' in video confirms the method exists,
and document.pictureInPictureEnabled confirms the browser has
not disabled the feature (e.g., it can be blocked by a
Permissions-Policy header). To exit, call
document.exitPictureInPicture().
The live demo below wires up both buttons on the same video element. Click "Fullscreen" to expand the player, or "Picture-in-Picture" to pop it into a floating overlay. Both buttons disable themselves automatically when the feature is unavailable.
For a complete example that combines a custom play/pause toggle, a scrubbing seek bar, volume control, and both Fullscreen and PiP into a single polished interface, see Example 01: Custom Player.