Imagine joining a video call and blocking camera access. A minute later, you decide you want to turn it on after all. There’s an “Enable camera” button on the page, but clicking it is no longer enough. The browser remembers your choice. How can the page help you allow access again?
How it works today #
We usually request camera access from JavaScript with getUserMedia().
const stream = await navigator.mediaDevices.getUserMedia({
video: true
});

If the user allows access, the method returns a MediaStream. If they click “Block,” the request fails with a NotAllowedError. Closing the prompt and blocking access are different actions, though. After the prompt is closed, the browser can usually show it again. If it has saved a denial, the next getUserMedia() call will fail without a new prompt. Media Capture and Streams specification
At that point, the developer has to explain where to find the site settings and how to allow the camera again. The user just wants to turn on their video, but now they’re dealing with browser settings. That adds stress before a call. And the developer needs clear instructions for different browsers.
Getting camera access back #
The authors of the media capture elements proposal want to make this easier. Someone who previously blocked the camera should be able to allow it again from the page. The browser itself will show the controls they need.
This is where <camera> comes in. It’s a built-in button for camera access. I first tried it in Chrome Canary (155.0.8048.0) and Beta (154.0.8037.0). While I was writing this article, it also became available in stable Chrome 153 (153.0.8010.37).
Before trying it, keep one requirement in mind. Like getUserMedia(), <camera> needs a secure context. Open the example over HTTPS, or use localhost if you’re testing on your own computer.
First a button, then a track #
Here’s the markup.
<camera></camera>

Despite its name, <camera> doesn’t display video. It shows a button that lets the user turn on the camera. Once the camera starts, the element fires a track event. The video track becomes available through camera.track, and we can use it for the preview. The draft specification has more details.
Let’s start with a small example. We’ll put <camera> and <video> on the page.
<camera id="camera"></camera>
<video id="preview" autoplay playsinline muted></video>
<script>
const camera = document.querySelector("#camera");
const preview = document.querySelector("#preview");
camera.addEventListener("track", () => {
const { track } = camera;
if (!track) return;
preview.srcObject = new MediaStream([track]);
});
</script>
Once the user allows access and the camera starts, the track handler runs. It takes the video track from camera.track, creates a MediaStream from it, and assigns the stream to preview.srcObject. The video then appears in <video>.
I also tested restoring access. First, I blocked the camera in the site settings, then clicked <camera>. The browser offered to let me allow access again. I didn’t have to go back to the settings. No extra code was needed either. The same element and handler did the job. A getUserMedia() call would have failed while that block was still in place.
Now let’s add a stop button.
<button id="stop" type="button" disabled>Stop camera</button>
We’ll replace the previous track handler and add a handler for the button. The button becomes available when we get a track. Clicking it stops the camera and clears the preview.
const stop = document.querySelector("#stop");
camera.addEventListener("track", () => {
const { track } = camera;
if (!track) return;
preview.srcObject = new MediaStream([track]);
stop.disabled = false;
});
stop.addEventListener("click", () => {
camera.track?.stop();
preview.srcObject = null;
stop.disabled = true;
});
Let’s add an event log below the preview. It will show when the camera starts, when a request is cancelled, and what error occurred.
<pre id="event-log" role="log"></pre>
In JavaScript, we’ll add a function to log the element’s state.
const eventLog = document.querySelector("#event-log");
function logState(label) {
const { track, error } = camera;
eventLog.textContent +=
`${label}\n` +
`track: ${track?.readyState ?? String(track)}\n` +
`error: ${error?.name ?? String(error)}` +
(error?.message ? ` (${error.message})` : "") +
"\n\n";
}
logState("Initial state");
for (const type of ["track", "cancel", "error"]) {
camera.addEventListener(type, () => logState(`event: ${type}`));
}
We’ll also replace the stop button handler so that stopping the camera gets logged.
stop.addEventListener("click", () => {
camera.track?.stop();
logState("action: stop()");
preview.srcObject = null;
stop.disabled = true;
});
Now the log shows the track’s state and the value of camera.error whenever one of these events fires or the stop button is clicked.
If the user closes the prompt or decides not to continue, the element fires a cancel event. If the request fails for another reason, it fires an error event, with details available in camera.error. Camera element specification
The stale error #
The preview worked, but the log helped me spot something odd in camera.error. I dismissed the prompt with its close button, then clicked the element again and allowed access, and finally stopped the track.
event: cancel
track: null
error: NotAllowedError (Permission dismissed)
event: track
track: live
error: NotAllowedError (Permission dismissed)
action: stop()
track: ended
error: NotAllowedError (Permission dismissed)
The camera was working, but camera.error still reported the earlier dismissal, even after the track had stopped. In these tests, a non-null error didn’t mean the latest attempt had failed.
The draft specification doesn’t explicitly clear the error when a new track becomes available either. This may be a question about the API’s intended behavior, rather than just Chrome’s implementation.
Our example uses the track event to receive the video track. The stale error is a reason to avoid treating camera.error as a status flag for the current attempt.
I reproduced this in both Chrome Canary and Chrome 153 stable. I filed Chromium issue #558064964 to describe what happened and ask whether the old error should be cleared after the camera starts successfully.
If you are reading this in Chrome 153 or newer, the demo below is a slightly fuller version of the same example. Dismiss the prompt once, then allow access, and watch what camera.error does.
Styling the button #
The browser draws the button and limits how it can be styled. Starting the camera requires a click from the user. Calling camera.click() from code isn’t enough. These restrictions help the browser check that the user clicked the button intentionally. Chrome 153 release notes
In my example, the button still worked with the colors, corner radius, font size, and padding shown below. This is one combination I tested; I haven’t explored the styling limits in detail.
camera {
border-width: 0;
border-radius: 20px;
font-size: 20px;
font-weight: bold;
color: white;
background-color: rebeccapurple;
padding-inline: 40px;
padding-block: 20px;
}

Browser support #
At the time of writing, Can I Use shows support for Chrome 153+ on desktop. Microsoft also lists <camera> in its Edge 153 release notes, with the stable release scheduled for September 10, 2026.

Browser support is still narrow, so I’d keep getUserMedia() as the main path for now. Using <camera> would also change the interface: the browser supplies the camera button and limits how it looks and behaves.
What interests me most is the permission recovery. In my test, someone who had blocked the camera could allow it again without leaving the page or following instructions through browser settings.
That makes <camera> worth experimenting with. I’d like to see whether this approach can work across browsers while giving sites enough control to fit the button into their interfaces.