The <usermedia> HTML Element
<usermedia> is a new HTML element, shipped in Chrome 151, that asks for camera and microphone access declaratively. You put the tag on the page, the browser draws the button, the user clicks it, and your page gets a MediaStream — no getUserMedia() call in your code. This page exercises every part of it: the stream, error and cancel events, the setConstraints() filter, the enforced CSS rules, the :granted pseudo-class, the sibling <camera> and <microphone> capability elements, legacy <permission> mode, permissions policy in iframes, and the getUserMedia() fallback — logging everything as it happens.
How to use
- Open this page in Chrome 151 or later over HTTPS or localhost.
- Click the browser-drawn button in The element, live below. Chrome — not this page — draws it and owns the click.
- Allow camera and microphone in the prompt. The preview and the track table fill in.
- Try the constraints lab before clicking: it shows you exactly which constraints Chrome keeps and which it throws away.
- Break the element on purpose in the styling playground and watch Chrome disable it.
- Watch the event & error log at the bottom the whole time.
What this demo covers
stream,errorandcancelevents, live- A constraint sanitizer that mirrors Chrome's own filter — see what survives
setConstraints() - Declarative constraints via a nested
<script>tag - The full CSS rulebook, with one-click violations you can trigger
:grantedstyling and the browser-supplied,lang-driven label- The
<camera>and<microphone>single-capability elements - Legacy
typemode:isValid,invalidReason, prompt events - Permissions policy in cross-document iframes
- A working
getUserMedia()fallback for Firefox and Safari - Complete event, track and error log with severity filters
Environment & support
1. The <usermedia> element, live
Below is a real <usermedia> element. Everything you see inside its box
is drawn by Chrome in a closed shadow root — the label text, the icon, the state. Your CSS
can tint it, but it cannot write it. The <button> nested inside is
fallback content: supporting browsers ignore it, everyone else gets it (and it is wired to
getUserMedia() — see section 9).
Nothing on this page can click that button for you. A scripted
element.click() is an untrusted event and fires error with an
InvalidStateError instead — there is a button in section 5 that proves it.
No video track in this stream.
Idle — no stream
Input level
- Element staterequest
- Tracks0
- Time to stream—
- stream events0
- cancel events0
- error events0
Stopping every track returns the element to its request state, and the next
click fetches a fresh stream. While a stream is set, clicks on
<usermedia> are ignored — unlike
<camera> and <microphone>, it has no second-click
mute behavior.
Live properties
Tracks in element.stream
No stream yet.
Raw track settings, capabilities & constraints (JSON)
{}2. setConstraints() and the constraint filter
setConstraints() takes an HTMLMediaStreamConstraints dictionary —
{ video: MediaTrackConstraintSet, audio: MediaTrackConstraintSet } — and hands
it to the getUserMedia() call Chrome makes on your behalf. It is not the same
thing as passing constraints to getUserMedia() yourself. Chrome runs your
dictionary through a filter first, and the filter is aggressive:
- Only a fixed list of properties survives. Anything else —
displaySurface,zoom,torch,suppressLocalAudioPlayback,advanced— is dropped on the floor. - Every surviving property must be a bare value. A
ConstrainRangeorConstrainDOMStringParametersobject —{ exact: … },{ ideal: … },{ min: …, max: … }— is discarded, key and all. This exists so the element can never fail with anOverconstrainedError, but it means the most common way of writing constraints silently does nothing. - The result always contains both an audio and a video entry, so
<usermedia>always requests both devices.audio: falsewill not give you a video-only stream. That is what the<camera>and<microphone>elements are for. setConstraints()only takes effect once. The first call wins; every later call on the same element returns without doing anything and without warning. Build the whole dictionary before you call it.
Call this before the first click on the element. Afterwards it has no effect.
What Chrome will actually send
Computed locally with the same rules as Chromium's
SanitizeTrackConstraints(). Green survives, red is dropped.
Filtered dictionary as JSON
{}The properties that survive
| Property | Applies to | Accepted form |
|---|
3. Declarative constraints without JavaScript
Constraints can also be written straight into the markup, as a JSON payload in a nested
<script type="permissionconstraints">. The type is not a real script
type, so nothing executes; the element reads the text content. This is not covered in
Chrome's announcement post, so treat it as best-effort — the probe below tells you whether
it did anything in your browser.
Click it, then compare the resulting track settings against the JSON above. A 640×480 video track with echo cancellation off means the markup was honoured.
4. Styling: what the browser lets you change
The element is a trusted surface, so Chrome polices its appearance. Two mechanisms are at work and they behave very differently:
- Correction. Out-of-range values are clamped or reset during style
adjustment. The element keeps working; you just do not get what you asked for.
font-weight: 100becomes 200, a negative margin becomes 0,font-style: oblique 40degbecomesnormal, andmax-heightlands at three times the font size no matter what you wrote. - Invalidation. Some violations make the element refuse to be clicked at
all: contrast below 3:1, a non-opaque
colororbackground-color, a font smaller thansmallor larger thanxxx-large, a forbiddendisplayvalue, or the element being occluded, clipped, off-screen or freshly attached. Chrome files a report in the DevTools Issues panel each time.
Edited live. Try a preset, or break it yourself.
Computed style is read back below, so you can see exactly what Chrome corrected.
Author value vs. computed value
| Property | You asked for | Browser computed |
|---|
The rulebook
| Property | Rule enforced by Chrome |
|---|
Everything not listed — border-*, outline-*,
width, height, position, top/right/bottom/left,
flex-*, z-index, visibility,
aspect-ratio, color-scheme, the font-synthesis family and their
logical equivalents such as inline-size — behaves normally.
background-image, border-image, mask,
clip-path, filter, cursor,
content-visibility, contain and the
corner-shape family are ignored outright.
5. State, the :granted pseudo-class and trusted clicks
:granted
usermedia:granted matches once permission is active and the stream has been
acquired. It is the only way to react to the element's state from CSS — there is no
:denied, and :invalid only exists in legacy mode. Standard
:hover and :active work as usual.
usermedia {
background-color: #ed341d;
color: #fff;
}
usermedia:granted {
background-color: #1f7a4d; /* live */
}Checking…
The browser writes the label, and lang picks the language
You cannot set the element's text. Chrome supplies it, translated from the inherited document language, and swaps it between the request and granted wording on its own. Change the language below and watch the element re-render — including its width.
Trusted activation
The element only acts on genuine user input. A synthetic click fails the
isTrusted check and produces an error event carrying an
InvalidStateError rather than a permission prompt. Chrome also refuses
activation for the first ~500 ms after the element is attached or after its attributes
change, and while it is occluded, clipped or outside the viewport.
Results appear here and in the log.
6. The <camera> and <microphone> elements
Same machinery, one capability each, and one genuinely new behavior: after a grant they do
not go idle, they become a mute toggle. A second click flips
track.enabled, and the browser-drawn label follows. They expose
track (a single MediaStreamTrack) instead of
stream, fire track instead of stream, and take a
flat MediaTrackConstraintSet — no video/audio
wrapper. These are what you want when you need video only or audio only, which
<usermedia> cannot give you.
No video track yet.
const cam = document.querySelector('camera');
// Flat constraint set — no { video: … } wrapper.
cam.setConstraints({ width: 1920, height: 1080, facingMode: 'environment' });
cam.addEventListener('track', () => {
video.srcObject = new MediaStream([cam.track]);
});7. Legacy mode: the type attribute
<usermedia> replaced the origin-trial
<permission type="camera microphone"> element. To keep those sites
working, adding a type attribute switches the element back to the old
<permission> behavior — a permission gate, not a stream mediator. It is
explicitly a migration shim and Chrome intends to deprecate it, so do not build on it.
Migrating means dropping type and swapping the
HTMLPermissionElement feature check for HTMLUserMediaElement.
Legacy mode does expose things standard mode has no equivalent for, which makes it the only
way to observe the element's own validity from script:
isValid, invalidReason, permissionStatus,
initialPermissionStatus, the static
HTMLUserMediaElement.isTypeSupported(), the :invalid
pseudo-class, and the promptaction, promptdismiss and
validationstatuschange events.
Cover the element and watch isValid flip to
false with an invalidReason — the anti-clickjacking check,
made visible. Pick a type the element does not recognise and it gives up entirely:
it drops into fallback mode, renders its nested button like an unknown tag would, and
reports type_invalid.
8. Iframes, permissions policy and cross-origin rules
The element is bound by the same permissions policy as
getUserMedia(), plus two rules of its own. Where the policy does not allow the
feature, the element goes permanently inert: it still renders, it still takes the click,
and then nothing happens — no prompt, no stream, no stream event, no
error. Granting the permission in the top-level document does not help.
- Same-origin frames already have it.
cameraandmicrophonedefault to aselfallowlist, so a same-origin child inherits them without anyallowattribute. You have to deny it explicitly —allow="camera 'none'; microphone 'none'"— which is what the left frame below does. - Cross-origin frames need both.
allow="camera; microphone"on the iframe and a Content-Security-Policy with aframe-ancestorsdirective served by the framed document. Without that CSP the element is invalid in any frame cross-origin to the outermost page, whatever the permissions policy says. - Fenced frames are out entirely, as are insecure contexts — over plain HTTP the element renders its fallback content instead.
Both frames report back here and into the log below. Click the element in each and compare.
Open this page over file:// and the frames will not load — serve it over HTTP.
9. Progressive enhancement and the getUserMedia() fallback
Browsers that do not know the tag parse it as an
HTMLUnknownElement and render its children; Chrome renders its own UI and
hides them. One markup, both audiences. Feature-detect with the interface name, not the
tag:
<usermedia id="stream-handler">
<button id="fallback-stream-handler">Enable camera and mic</button>
</usermedia>if ('HTMLUserMediaElement' in window) {
document.getElementById('stream-handler')
.addEventListener('stream', handleStream);
} else {
document.getElementById('fallback-stream-handler')
.addEventListener('click', () => {
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(handleStream);
});
}Your current configuration, as code
Generated from the constraints and CSS you set above. Copy it into your own page.
10. <usermedia> vs. getUserMedia()
| Feature | getUserMedia() | <usermedia> |
|---|---|---|
| What triggers the prompt | Your script, whenever it runs | A user click on a browser-drawn control |
| Browser's role | Applies heuristics and quiet blocks to decide whether to even show a prompt | Data mediator — owns consent, the call, and delivery of the stream |
| Your code | Call the API, chain the promise, handle every rejection name | Listen for stream, read .stream |
| Recovering from a previous denial | Send the user into browser settings and hope | In-page recovery flow on the next click |
| Quiet blocks after repeated dismissals | Request can silently never prompt | Bypassed — the click is a trusted intent signal |
| Constraints | Full MediaStreamConstraints, including exact, ranges and advanced | Filtered allowlist of bare values only |
| Audio-only or video-only | { video: true, audio: false } | Not possible — use <camera> or <microphone> |
| Device picking | deviceId: { exact: … } after enumerateDevices() | deviceId as a bare string, best-effort |
| Errors you can get | NotAllowed, NotFound, NotReadable, Overconstrained, Security, Type… | Same minus Overconstrained, plus a separate cancel event for dismissal |
| Appearance | Yours entirely | Browser-drawn, tintable within limits |
| Support | Every modern browser | Chrome 151+ on desktop and Android, not WebView |
Reported from Chrome's origin trial: Cisco saw permission recovery after an initial denial go from about 10% to over 65%. Zoom measured a 46.9% drop in camera and microphone capture errors. Google Meet saw 17% less "mic not working" feedback and a 131% increase in successful recovery.
Event & error log
No events yet.
Every stream, error and cancel event from every
element on this page, every MediaStreamTrack state change
(mute, unmute, ended), every Permissions API
change, every style validation result and every uncaught page error is recorded here with
the elapsed time since page load. Enable Debug for the per-frame noise.
Chrome's own complaints about your CSS go to the DevTools Issues panel,
not here.
Frequently asked questions
What is the <usermedia> HTML element?
A built-in HTML element, shipped in Chrome 151, that requests camera and microphone access
and hands your page the resulting MediaStream. The browser renders the
control and owns the click, so the permission prompt is guaranteed to be attached to a real
user action. It is part of Chrome's Capability Elements suite, which grew out of the
<permission> element (PEPC) proposal and now also includes
<geolocation>, <camera> and
<microphone>.
Does <usermedia> replace getUserMedia()?
No. getUserMedia() stays the primary programmatic API and is explicitly a
non-goal to replace. The element covers the common case where a user clicks something to
turn their camera on; anything needing exact constraints, device switching mid-call, or
audio-only capture still goes through the JavaScript API.
Which browsers support the <usermedia> element?
Chrome 151 and later, on desktop and Android. Not Android WebView — the element needs browser-level permission UI that WebView delegates to its embedder. Firefox has the proposal under consideration and Safari has not given a position. Everywhere else the tag is an unknown element and your fallback content renders instead.
Can I use <usermedia> for video only or audio only?
No. The constraint filter always produces both an audio and a video entry, so the element
always asks for both devices. audio: false is discarded. Use
<camera> for video-only or <microphone> for
audio-only.
Why are my constraints being ignored?
Almost always because they are wrapped. { width: { ideal: 1280 } } is dropped
entirely; { width: 1280 } is kept. The filter also drops any property outside
its allowlist, and setConstraints() only takes effect on its first call.
Section 2 above shows you which of your constraints survive.
Why did my element stop responding to clicks?
Chrome disables the element when it cannot vouch for the click: contrast under 3:1, a
translucent color, a font outside the small–xxx-large range, a
forbidden display value, something overlapping it, it being clipped or
scrolled out of view, or it having been attached or modified within the last half second.
The DevTools Issues panel names the exact violation.
Can I change the button's text?
No. The label is browser-supplied and localized from the inherited
lang, and it changes by itself between the request and granted states. That is
the point — a label the page cannot write is a label the user can trust.
Does clicking the element skip the permission prompt?
No. The click is a stronger signal of intent, which lets Chrome bypass the quiet blocks that suppress script-triggered prompts and offer in-page recovery after a denial, but the user still decides.
Works on
- Chrome 151+ on Windows, macOS, Linux, ChromeOS and Android —
<usermedia> - Chrome 153+ —
<camera>and<microphone>. Earlier builds can enable them with theCameraAndMicrophoneElementsflag underchrome://flags/#enable-experimental-web-platform-features - Chrome 144+ — the related
<geolocation>capability element - Secure contexts only: HTTPS, or
localhost/127.0.0.1 - Everywhere else: the fallback
<button>andgetUserMedia()
Known issues
- No video-only or audio-only stream from
<usermedia>. The camera light comes on even if you only wanted a microphone setConstraints()is one-shot and silent about it — a second call is discarded with no warning, no exception and no console message- Wrapped constraints (
exact,ideal,min,max) are dropped silently, which makes reliable device selection and resolution pinning hard - No declarative way to connect the element to a
<video>. You still need JavaScript to setsrcObject - No
autostartattribute in the shipped element. It is in the explainer, not in Chrome 151 — the element has no way to resume a previously granted stream without a click - No way to read the element's own validity in standard mode.
isValidandinvalidReasonexist only in the deprecatedtypelegacy mode - A stream starts live. There is no way to acquire it with tracks disabled, so the hardware indicator lights up the moment permission is granted
- In a cross-origin iframe the element is invalid unless the framed document also serves
a Content-Security-Policy containing
frame-ancestors— a requirementgetUserMedia()does not have, and one that quietly breaks embedded widgets - When it is invalid — blocked by permissions policy, occluded, badly styled — clicks produce nothing at all. No event, no exception, no console message, so there is no way for your code to notice
- Not available in Android WebView, which rules out a large slice of in-app browsers
- The element is not focusable from script and is skipped by
autofocuswithout user activation, so keyboard-first flows need care - Chrome-only for now: no Firefox or Safari implementation
RELATED RESOURCES
- Blog post: Introducing the <usermedia> HTML element — Chrome for Developers
- GitHub code: Media Capture Elements explainer: <camera>, <microphone>, <usermedia>
- W3C draft: Specification — Media Capture and Streams Extensions
- Blog post: Introducing the <geolocation> HTML element
- Chrome blog: The original <permission> element origin trial (PEPC)
- Chrome platform status: <usermedia> on Chrome Platform Status
- Blog post: <camera> and <microphone> in Chrome 153
- GitHub code: github.com/addpipe/usermedia-html-element-demo
- Web platform tests: User Media Permission Tests (Experimental Browsers)
- Blog post: Getting Started With getUserMedia