A toggle that stops the screen going dark while somebody is looking at it but not touching it, and — unlike every version written by hand — keeps on being true about whether the screen is actually being held awake. Reach for it wherever the page is being read rather than operated: a recipe followed with both hands busy, a barcode, QR code or boarding pass held up at a till or a gate, sheet music or guitar tabs on a stand, a presentation or kiosk view, a workout, cooking or interval timer counting down, an inspection or picking checklist walked through on a phone, turn-by-turn directions, a live scoreboard or auction, a long article or PDF being read, a dashboard left up on a wall display, a video call or a camera feed. Common asks it answers: "keep screen awake react", "prevent screen from sleeping web", "screen wake lock react", "wake lock api react", "navigator.wakeLock react", "useWakeLock hook", "react-screen-wake-lock alternative", "NoSleep.js alternative", "stop phone screen turning off website", "keep display on pwa", "wake lock toggle shadcn", "shadcn keep screen on", "wake lock released when tab hidden", "wake lock stops working after switching tabs", "wakeLock request NotAllowedError", "navigator.wakeLock is undefined", "wake lock not working on http", "screen-wake-lock permissions policy iframe", "release wake lock on unmount", "画面が消えないようにする react", "スリープ防止 ウェブ", "スクリーンスリープ 無効化 react", "タブを戻すと画面が消えてしまう". Official shadcn/ui has nothing of the kind, and the measurement is not close. Fetching all sixty-three registry entries today — sixty-two are fetchable, questionnaire alone 404s, and nine of the newer ones are only served on the new-york-v4 style track, so a probe of new-york and default silently misses them — and concatenating the component sources themselves rather than their JSON envelopes gives 223,287 bytes. In it, wakeLock, WakeLock, requestWakeLock, WakeLockSentinel, screen-wake-lock, keepAwake, NoSleep, visibilityState and visibilitychange are every one of them zero hits. Official has no component that reacts to the tab being hidden at all, so an agent asked for this writes it from scratch, and the version it writes has one specific bug in it. The bug is a single `const [isOn, setIsOn] = useState(false)` flipped inside the click handler, which makes the toggle a claim about the lock rather than a reading of it. It demos perfectly, because a demo never leaves the tab. Then the user glances at a message and comes back — and the browser releases a screen wake lock the moment the document stops being visible, silently, with no callback and nothing in the console. The toggle still reads "Screen stays on"; the phone in their hands starts dimming on schedule. Nothing in the UI ever admits it. That is the failure this component is built around: the lock has to be taken again on the way back, and the way back is `visibilitychange`. So the state here is two things, not one. `isEnabled` is what the user asked for and it survives the hidden stretches; `isActive` says whether a sentinel is being held right now, and it is written only by the sentinel’s own `release` event — the event the browser fires when the tab is hidden, the window is minimised, the battery gets low or the OS simply takes it back. On the element they appear as `aria-pressed` and `data-active`, so `aria-pressed="true"` with `data-active="false"` is a readable, honest state rather than a lie. Note what this does not need, because it is the exact opposite of the Fullscreen API and the difference is load-bearing: `wakeLock.request()` does not require a user gesture. It requires the document to be visible. That is what makes the re-acquire possible at all — there is no click to hang it on when somebody switches back to a tab — and it is also why the request fails in places a click would have got through. Asking while hidden is a guaranteed NotAllowedError, so it is not asked; and a NotAllowedError that arrives because the tab went away mid-request is a race, not a refusal, so it is not reported — a version that surfaces it puts an error in front of the user every time they switch tabs. A refusal that arrives while the page is visible is the real thing — an iframe that was not granted `allow="screen-wake-lock"`, or a browser that has decided no — and there the setting goes back off and `onWakeLockError` fires, rather than retrying on a timer that would spin forever. TypeScript will not help you here and will in fact mislead you. Since TS 5.x, lib.dom.d.ts declares `readonly wakeLock: WakeLock` on Navigator — not optional — so `navigator.wakeLock.request("screen")` type-checks cleanly and then throws `TypeError: Cannot read properties of undefined` at runtime wherever it is absent. This is a secure-context API, so that includes every page served over plain http: the staging box on an internal IP, the phone opening your dev server by LAN address. Detection is written by hand with an `in` check, it never reaches the render — so the server and the first client render agree and nothing hydrates twice — and where the API is missing the control stays in the tab order with `aria-disabled` and an accessible name that explains itself, rather than taking the `disabled` attribute that would remove it from the tab order so nobody ever hears why. The rest is the lifecycle nobody gets to on the first pass. Only one request is ever in flight, because a hidden/visible flap can fire two before the first settles and every sentinel but the last would be leaked, held with nothing left pointing at it. A sentinel that arrives after the user has switched the toggle off is released immediately instead of kept. Unmounting hands the lock back, because the sentinel is owned by the document rather than by the component: navigating from the recipe to the checkout inside a single-page app would otherwise leave the screen pinned awake with no control left to turn it off. The API: `WakeLockToggle` takes `defaultEnabled`, `onLabel`, `offLabel`, `unsupportedLabel`, `iconOnly`, `onEnabledChange` and `onWakeLockError` — named that way rather than `onChange` and `onError` because both of those are native DOM attributes React defines on every element, and a prop by either name collides the moment the options are spread onto the button. `useWakeLock()` returns `{ isEnabled, isActive, isSupported, error, enable, disable, toggle }` for a control you lay out yourself, and `isWakeLockSupported()` is there when you would rather hide your own. Within pulld it sits with the other components that watch what the browser is doing behind the app rather than what the user is doing in it: idle-timeout is the mirror image, ending a session when nobody is there, network-status reports a connection that changed without being asked, countdown and save-status are the things most often on screen while this is on, and fullscreen-button is the other half of a kiosk or presentation view. Distinct from a CSS or meta-tag trick: this is the browser’s own screen wake lock, and it lets the display dim on the browser’s terms the moment the setting is turned off. One file, one dependency (lucide-react for the two icons), every colour a shadcn token, so light and dark follow on their own.
pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/wake-lock-toggle.json"