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.
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 Edge84 and later
- Opera70 and later
- Samsung Internet14 and later
- Firefox126 and later (May 2024)
- Safari on Mac, iPhone and iPad16.4 and later (March 2023)
- iPhone and iPad Home Screen web appsiOS 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.
wake-lock.jslet 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 activeThe 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 itCommon in an iframe without
allow="screen-wake-lock". AwakeTab shows "Blocked — here's the fix". - Safari has not had a tap yetSafari 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 chargingFirefox refuses new locks and releases held ones. Plug in, then tap Retry.
- The page is not on HTTPSThere 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 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.
Ready moves to Starting when the page calls request. Starting moves to Screen awake when the browser grants the lock. Screen awake moves to Paused, tab hidden, when the page is hidden. When the page is visible again it requests a new lock and returns to Starting.
- Readyrequest()Starting…grantedScreen awake
- Screen awaketab hiddenPaused — tab hiddenvisible againStarting…
- Starting…NotAllowedErrorBlocked — here's the fix
- Readyno wakeLock in navigatorTap to use the fallbacktapAwake via video fallback
- Screen awakerelease()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.
embed.html<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.
headers.txtPermissions-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.
stop.jsasync 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.
Questions
Soubhik Biswas · Builds AwakeTab. Sources checked Saturday, 26 September 2026. About AwakeTab