Hooks

useFullscreen

A hook that puts a single element into and out of the browser's native fullscreen mode, keeping a `fullscreen` boolean in sync — including when the user presses Esc.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

export interface UseFullscreenResult<T extends HTMLElement> {
  /** Callback ref — attach it to the element you want to be able to fullscreen. */
  ref: (node: T | null) => void
  /** Whether the tracked element is currently the document's fullscreen element. */
  fullscreen: boolean
  /** Requests fullscreen on the tracked element. Never throws — resolves `false` on
   *  any failure (no node attached, unsupported browser, or the browser rejected the
   *  request because it wasn't called synchronously inside a user gesture). */
  enter: () => Promise<boolean>
  /** Exits fullscreen. Never throws — resolves `false` if nothing is fullscreen, the

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/use-fullscreen.json

Prompt

Build a React + TypeScript "useFullscreen" hook (no dependencies beyond React;
uses the browser's Fullscreen API — `Element.requestFullscreen`,
`document.exitFullscreen`, `document.fullscreenElement`, and the
`"fullscreenchange"` event).

Contract
- `useFullscreen<T extends HTMLElement = HTMLElement>(): { ref: (node: T |
  null) => void; fullscreen: boolean; enter: () => Promise<boolean>; exit:
  () => Promise<boolean>; toggle: () => Promise<boolean>; supported: boolean }`.
- No options object — the generic type parameter just picks the element type
  the callback ref accepts.

Behavior
- `ref` is a callback ref (not `useRef` + a mount `useEffect`) that stores the
  current node in a ref. It doesn't build a per-node observer or listener —
  fullscreen state is a single document-level thing, not per-element.
- `fullscreen` is driven by `useSyncExternalStore`: `subscribe` registers a
  `document` `"fullscreenchange"` listener (guarded for `typeof document ===
  "undefined"`), `getSnapshot` compares `document.fullscreenElement` against
  the ref's current node, `getServerSnapshot` returns `false`. Zero manual
  `useEffect` + `setState`.
- `enter()` calls `node.requestFullscreen()` on the tracked node, wrapped so
  it never throws: resolves `false` if there's no node attached, the browser
  doesn't support the API, or the returned promise rejects (most commonly:
  `enter()` wasn't invoked synchronously inside a real user gesture, or the
  browser/embedder denied the request).
- `exit()` calls `document.exitFullscreen()`; resolves `false` if nothing is
  currently fullscreen or the browser is unsupported, otherwise resolves the
  outcome the same non-throwing way.
- `toggle()` calls `exit()` if `fullscreen` is currently `true`, otherwise
  `enter()`.
- Esc: the browser exits fullscreen on its own and fires `"fullscreenchange"`
  itself — the existing subscription picks that up automatically, so there
  is no separate Esc keydown handler to write.
- `supported` reads `document.fullscreenEnabled`; `false` during SSR and on
  unsupported browsers.
- No vendor-prefixed (`webkit*`) fallback — targets the unprefixed,
  standardized Fullscreen API baseline (every evergreen browser, including
  Safari 16.4+).

Rendering & styling
- The hook renders nothing itself. Consumers own the fullscreened element's
  markup entirely; once it's actually fullscreen, the browser's UA
  stylesheet stretches that element to fill the viewport — no `fixed
  inset-0` classes needed from the consumer to achieve that.
- Any "entering/exiting" affordance (an icon/label swap on a button) is the
  consumer's job, using semantic tokens (`bg-primary`, `text-muted-foreground`)
  and respecting `prefers-reduced-motion` for any transition.

Customization levers
- `:fullscreen` CSS pseudo-class — style the tracked element differently
  while it's the active fullscreen element (e.g. `[&:fullscreen]:bg-background`)
  without needing any extra hook state.
- Double-click to toggle — wire `onDoubleClick={() => toggle()}` on the
  element (or a header inside it) alongside an explicit button; a common
  pattern for video players.
- Vendor-prefixed fallback (`webkitRequestFullscreen`/`webkitExitFullscreen`/
  `webkitFullscreenElement`) — only layer this in if you must support
  pre-16.4 Safari or certain embedded WebViews; the default hook
  intentionally omits it to stay standards-only.
- Fullscreen options — pass `{ navigationUI: "auto" | "hide" | "show" }` into
  a wrapped `enter()` if you need to control browser UI visibility during
  fullscreen (rarely needed).

Concepts

  • Callback ref, not an effect-tracked ref — the ref callback just stores the current node; there's nothing to (re)connect per node, since the thing being subscribed to (fullscreenchange) lives on document, not on the element.
  • Document-level fullscreenchange as the single source of truth — Esc-triggered exits, exits triggered by other code, and this hook's own enter()/exit() calls all reconcile through the exact same event, so Esc never needs a separate handler.
  • User-gesture requirementrequestFullscreen() must be invoked synchronously inside a real user interaction (a click/keydown handler); calling it after an await or inside a setTimeout gets it silently rejected by the browser. enter() surfaces that as a non-throwing false, never an unhandled rejection.
  • Non-throwing Promise contractenter/exit/toggle always resolve to a boolean, so callers branch on the result directly instead of wrapping every call in try/catch.
  • Fullscreen ≠ CSS "looks fullscreen" — this hook drives the real browser Fullscreen API (browser chrome hides, <video>/<canvas> can hardware-scale); a fixed inset-0 layout is a different, purely visual technique with none of that native behavior.

On This Page