The Pipe Platform Achieves Security and Compliance Milestone with SOC 2 Type I Attestation. Learn More

How to use

  1. Open this page in Chrome 151 or later over HTTPS or localhost.
  2. Click the browser-drawn button in The element, live below. Chrome — not this page — draws it and owns the click.
  3. Allow camera and microphone in the prompt. The preview and the track table fill in.
  4. Try the constraints lab before clicking: it shows you exactly which constraints Chrome keeps and which it throws away.
  5. Break the element on purpose in the styling playground and watch Chrome disable it.
  6. Watch the event & error log at the bottom the whole time.

What this demo covers

  • stream, error and cancel events, 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
  • :granted styling and the browser-supplied, lang-driven label
  • The <camera> and <microphone> single-capability elements
  • Legacy type mode: 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 state
    request
  • Tracks
    0
  • Time to stream
  • stream events
    0
  • cancel events
    0
  • error events
    0

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 ConstrainRange or ConstrainDOMStringParameters object — { exact: … }, { ideal: … }, { min: …, max: … } — is discarded, key and all. This exists so the element can never fail with an OverconstrainedError, 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: false will 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

PropertyApplies toAccepted 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: 100 becomes 200, a negative margin becomes 0, font-style: oblique 40deg becomes normal, and max-height lands 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 color or background-color, a font smaller than small or larger than xxx-large, a forbidden display value, 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

PropertyYou asked forBrowser computed

The rulebook

PropertyRule 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. camera and microphone default to a self allowlist, so a same-origin child inherits them without any allow attribute. 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 a frame-ancestors directive 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()

FeaturegetUserMedia()<usermedia>
What triggers the promptYour script, whenever it runsA user click on a browser-drawn control
Browser's roleApplies heuristics and quiet blocks to decide whether to even show a promptData mediator — owns consent, the call, and delivery of the stream
Your codeCall the API, chain the promise, handle every rejection nameListen for stream, read .stream
Recovering from a previous denialSend the user into browser settings and hopeIn-page recovery flow on the next click
Quiet blocks after repeated dismissalsRequest can silently never promptBypassed — the click is a trusted intent signal
ConstraintsFull MediaStreamConstraints, including exact, ranges and advancedFiltered allowlist of bare values only
Audio-only or video-only{ video: true, audio: false }Not possible — use <camera> or <microphone>
Device pickingdeviceId: { exact: … } after enumerateDevices()deviceId as a bare string, best-effort
Errors you can getNotAllowed, NotFound, NotReadable, Overconstrained, Security, Type…Same minus Overconstrained, plus a separate cancel event for dismissal
AppearanceYours entirelyBrowser-drawn, tintable within limits
SupportEvery modern browserChrome 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 smallxxx-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

Known issues

RELATED RESOURCES