Docs
Screen Wake Lock API: a practical guide with error handling
navigator. asks the browser to keep the display on. It works only on a secure (HTTPS) page that is visible. The browser releases the lock when the page is hidden and never asks again by itself. This guide gives you code for each step, the reasons a request fails, and support as of 26 September 2026.
On this page
Which browsers support it?
Versions as of 26 September 2026. Support claims come from browser documentation and engine source; real-device results appear in the wake lock browser support matrix once recorded, with platforms, notes and sources for each row.
- Chrome and Edge 84 and later
- Opera 70 and later
- Samsung Internet 14 and later
- Firefox 126 and later (May 2024)
- Safari on Mac, iPhone and iPad 16.4 and later (March 2023)
- iPhone and iPad Home Screen web apps iOS 18.4 and later
How do I ask for a wake lock?
Call request('screen') from a visible page served over HTTPS. Keep the object it returns: it is your only proof that the lock is held, and its release event tells you when it is gone. The wakeLock property only exists in secure contexts, so check with 'wakeLock' in navigator rather than calling and catching: calling request on undefined throws a TypeError that looks like a bug in your code. https:// pages and http://localhost count as secure.
let sentinel = null;let wanted = false;async function keepScreenOn() {wanted = true;if (!('wakeLock' in navigator)) return 'unsupported';try {const s = await navigator.wakeLock.request('screen');s.addEventListener('release', () => {if (sentinel === s) sentinel = null;});sentinel = s;return 'held';} catch (err) {// NotAllowedError: page hidden, Permissions-Policy,// no tap yet in Safari, or Firefox at 5% battery or lessconsole.warn(`${err.name}: ${err.message}`);return 'refused';}}startButton.addEventListener('click', keepScreenOn);
Start the request from a click handler: that click is the tap Safari needs. The wanted flag separates “the user asked for the screen to stay on” from “the browser currently holds a lock”. The browser can drop the second at any time; only the user should change the first. @awaketab/wake wraps this pattern, with a gesture-aware video fallback and a change event every time the state moves. Its source is MIT on GitHub, and the npm package is coming soon; the @awaketab/wake library page runs a live demo.
Why did request() throw NotAllowedError?
The browser rejects the promise with NotAllowedError for a small set of reasons. Battery saver modes are not one of them in Chromium or Safari. Here is what each cause looks like and what AwakeTab shows.
- The page is hidden or not active The request is rejected, and a held lock is released. AwakeTab shows "Paused — tab hidden" and asks again when you come back.
- A Permissions-Policy blocks it Common in an iframe without
allow="screen-wake-lock". AwakeTab shows "Blocked — here's the fix". - Safari has not had a tap yet Safari wants a recent tap on the page. AwakeTab shows "Blocked — here's the fix", and tapping Retry is that tap.
- Firefox is at 5 % battery or less and not charging Firefox refuses new locks and releases held ones. Plug in, then tap Retry.
- The page is not on HTTPS There is no
navigator., so nothing is rejected. AwakeTab calls this unsupported and offers the video fallback after a tap.wakeLock
Why a request fails, by engine
Checked against engine source on 26 September 2026.
| Cause | Chromium (Chrome, Edge, Opera, Samsung Internet) | WebKit (Safari) | Gecko (Firefox) |
|---|---|---|---|
| Page hidden or tab not active | NotAllowedError; a held lock is released | Same | Same |
Permissions-Policy or iframe without allow | NotAllowedError | NotAllowedError | NotAllowedError |
| No recent user gesture | No check | NotAllowedError without a recent tap or an earlier gesture | No check |
| Battery at or below 5 %, unplugged | No check | No check | NotAllowedError; a held lock is released |
| Battery saver, Energy Saver or Low Power Mode | No check | No check | No check beyond the 5 % rule |
| Plain http page | navigator. is undefined | Same | Same |
Sources: Chromium wake_lock.cc, WebKit WakeLock.cpp and Firefox WakeLockJS.cpp.
The battery-saver row matters because many pages, including earlier versions of ours, blamed battery savers for NotAllowedError. The engines don’t check for them. A saver can still change how long the device waits before the display goes dark, but that happens in the operating system’s settings, outside the browser, and your promise never rejects because of it.
What happens when the tab is hidden?
Switching tabs or apps, minimising the window or locking the phone hides the document. A visible window that has lost focus keeps its lock. The sentinel fires release, and your page should show that honestly instead of a running timer. The same event fires when you call release() yourself and when the browser takes the lock back for its own reasons.
The browser releases the lock the moment the page is hidden, and it never asks again by itself. Listen for visibilitychange and request a new lock when the page is visible again.
document.addEventListener('visibilitychange', () => {if (wanted && !sentinel && document.visibilityState === 'visible') {keepScreenOn();}});
Safari usually accepts that second request thanks to the earlier tap; if it rejects, show a button so the next tap can ask.
What does the whole lifecycle look like?
Each box is one of AwakeTab’s pill messages. The dashed line is the step most pages forget: asking again when the tab comes back.
Starting… NotAllowedError Blocked — here's the fix
Ready no wakeLock in navigator Tap to use the fallback tap Awake via video fallback
Screen awake release() Ready
Can a page in an iframe ask for one?
Only if the parent page allows it. The screen-wake-lock feature is allowed for the page’s own origin by default; a cross-origin iframe needs the embedding page to delegate it with allow="screen-wake-lock". Without it, the Permissions-Policy blocks the request and it fails with NotAllowedError, which is why “iframe without allow” is the first thing to check when a widget works on its own and fails when embedded.
<iframe src="https://widget.example.com/timer" allow="screen-wake-lock"></iframe> A site can also switch the feature off, or allow named origins, with a response header. screen-wake-lock=() blocks it everywhere on that page, including in the top-level document.
Permissions-Policy: screen-wake-lock=(self "https://widget.example.com") How do I let go on purpose?
When the user stops or the time is up, release the lock and clear the flag that says they wanted it. Otherwise your visibilitychange handler will ask again the next time the tab is shown.
async function letScreenSleep() {wanted = false;const s = sentinel;sentinel = null;await s?.release();}stopButton.addEventListener('click', letScreenSleep);
The video fallback trade-off
Before the API, libraries kept screens on by playing a tiny silent video in a loop. It still works as a fallback for browsers without the API, with three costs. It needs a user gesture, because browsers block video autoplay until someone interacts. It uses more power than a native lock, since the media pipeline stays busy. And your code can’t easily tell whether it is working, only whether play() resolved. Use it only when wakeLock is missing, never as a second attempt after a NotAllowedError. NoSleep.js vs @awaketab/wake compares two libraries that handle this differently.
What can a wake lock not do?
It keeps the display on while the page is visible. It can’t hold from a hidden tab, stop a laptop sleeping when the lid closes, or change an operating system rule such as a work sign-in lock. It sends no input, so it doesn’t change a chat app’s Away timer. On desktop Chrome or Edge, an extension with the power permission, such as AwakeTab for Chrome, can keep the screen on from a hidden tab; a web page can’t.
Try it
Start a session, switch tabs, then come back and watch the pill.
Questions
Does the Screen Wake Lock API show a permission prompt?
Not in Chromium: its screen-wake-lock permission is allowed by default, so a visible, top-level HTTPS page gets the lock without asking. Safari's condition is a recent tap on the page, and Firefox's is a battery above 5 % or a charger.
Why does the release event fire when my code never called release()?
The browser releases the lock when the page becomes hidden, and Firefox also releases it when the battery drops to 5 % or less while unplugged. Check document.visibilityState in the handler: if the page is still visible, the browser took the lock back for another reason.
Does a wake lock work on localhost?
Yes. Browsers treat http://localhost as a secure context, so navigator.wakeLock exists there. A plain http:// address on your local network does not count, so test phones over HTTPS or through a tunnel.
Can a service worker or web worker hold a wake lock?
No. The WakeLock interface is exposed on the window only, and the lock is tied to a visible document. A worker can't ask for one, and a page can't keep one while it is in the background.
Is it a problem to call request() while I already hold a lock?
It works: each call returns a new sentinel, and the screen stays on while any of them is unreleased. Keep one reference and check sentinel.released before asking again, so stray sentinels don't keep the screen on after you meant to stop.