vue-screenfull provides reactive, strongly typed fullscreen controls for Vue 3, with SSR-safe
imports and an optional CSS fallback when native element fullscreen is unavailable.
npm install vue-screenfull
vue-screenfull provides the Vue composables, component, directive, and plugin, and requires Vue 3
at runtime. Vue remains an optional peer dependency so vue-screenfull/browser can run without it.
The browser IIFE, lib/vue-screenfull.min.js, exposes VUE_SCREENFULL and expects a global Vue.
<script setup lang="ts">
import { ref } from "vue";
import { useScreenfull } from "vue-screenfull";
const target = ref<HTMLElement | null>(null);
const { isEnabled, isFullscreen, error, toggle } = useScreenfull();
</script>
<template>
<section ref="target">
<p>Fullscreen content</p>
<button type="button" :disabled="!isEnabled" @click="toggle(target)">
{{ isFullscreen ? "Exit fullscreen" : "Enter fullscreen" }}
</button>
<p v-if="error" role="alert">{{ error.message }}</p>
</section>
</template>
Call request or toggle directly from a click, keyboard, or touch handler. Browsers normally
require transient user activation and can reject a request even when isEnabled.value is true.
<script setup lang="ts">
import { ref } from "vue";
import { useScreenfullTarget } from "vue-screenfull";
const panel = ref<HTMLElement | null>(null);
const { request, isFullscreen } = useScreenfullTarget(panel);
</script>
<template>
<section ref="panel">
<button type="button" @click="request()">Open panel</button>
<span>Active: {{ isFullscreen }}</span>
</section>
</template>
useScreenfull().request() with no target opens document.documentElement.
A selector such as request("#player") is resolved with document.querySelector. Invalid or
unmatched selectors return INVALID_TARGET.
const result = await toggle(target, { navigationUI: "hide" });
if (!result.ok) console.warn(result.error.code, result.error.suggestion);
For image content, pass the image template ref. An HTMLVideoElement is also a valid target. If a
video presentation needs custom controls, target a wrapper that contains both the video and those
controls so they remain available in native or fallback fullscreen. Some mobile browsers offer
browser-managed video fullscreen independently of arbitrary-element fullscreen.
<button type="button" @click="exit">Exit fullscreen</button>
Keep a visible exit button inside the fullscreen target, particularly when fallback is enabled. Escape often exits fullscreen, but its browser behavior cannot be overridden reliably. Native change events reflect exits initiated through the browser UI.
const { isEnabled, status } = useScreenfull();
// isEnabled.value: native API currently enabled
// status.value: idle | requesting | fullscreen | exiting | fallback | unsupported | error
Feature detection is more reliable than user-agent checks across Safari, iOS/iPadOS, and WebViews.
const result = await request(target);
if (!result.ok) {
console.error(
result.error.code,
result.error.message,
result.error.suggestion,
);
}
Errors distinguish unsupported or SSR environments, invalid or detached targets, user activation,
permissions, iframe policy, pending transitions, native request or exit failures, and fallback
failures. error keeps the last error until clearError() is called.
const { request, exit, isFallback } = useScreenfull({
fallback: "css",
fallbackClass: "my-pseudo-fullscreen",
lockScroll: true,
restoreFocus: true,
});
CSS fallback fixes an HTMLElement to the visual viewport and preserves every inline style that it
changes. For element targets, it locks and restores background body scrolling. It keeps whole-page
targets scrollable, preserves the scroll position, adds the configured class, exits on Escape when
possible, and restores focus. Cleanup also runs when its Vue scope is disposed.
This mode is pseudo-fullscreen. It cannot hide address bars, browser controls, notifications, or operating-system UI. Keep an accessible exit button inside the target:
<button type="button" @click="exit">Close full-page view</button>
A custom fallback implements enter(context) and exit(context) and is responsible for complete
cleanup.
<Screenfull
target="#article"
fallback="css"
v-slot="screenfull"
@error="report"
>
<article id="article">
<button type="button" @click="screenfull.toggle()">
{{ screenfull.isFullscreen.value ? "Exit" : "Open article" }}
</button>
</article>
</Screenfull>
The renderless component emits change, enter, exit, error, and fallback. Its default slot
receives all composable refs and actions without imposing a visual system. Because screenfull is a
scoped-slot object, access its nested refs with .value.
<button v-screenfull>Fullscreen page</button>
<button v-screenfull="target">Toggle target</button>
<button v-screenfull:request="target">Enter target</button>
<button v-screenfull:exit>Exit</button>
<button
v-screenfull="{ target, action: 'toggle', options: { navigationUI: 'hide' } }"
>Toggle</button>
Only request, exit, and toggle arguments are supported. The default is toggle.
Directives need local registration unless the plugin is installed:
const vScreenfull = importedDirective;
import { createApp } from "vue";
import VueScreenfull from "vue-screenfull";
import App from "./App.vue";
createApp(App).use(VueScreenfull).mount("#app");
This registers Screenfull and v-screenfull. Use componentName and directiveName to change
their names. Named composable imports do not require plugin installation and remain tree-shakable.
Use vue-screenfull/browser when the caller does not own a Vue root. This subpath exports only
createScreenfullController, detectFullscreenApi, and their framework-neutral types. It does not
load Vue or Mazey at runtime.
With npm:
import {
createScreenfullController,
type ScreenfullChangeListener,
} from "vue-screenfull/browser";
const controller = createScreenfullController({ restoreFocus: true });
const target = document.querySelector("#player");
const button = document.querySelector("#toggle-fullscreen");
const toggle = () => controller.toggle(target);
const onChange: ScreenfullChangeListener = (state) => {
console.log(state.isFullscreen, state.element);
};
button?.addEventListener("click", toggle);
controller.on("change", onChange);
async function dispose() {
button?.removeEventListener("click", toggle);
controller.off("change", onChange);
await controller.destroy();
}
// Call dispose() when the integration is disposed.
As a native browser ES module:
<script type="module">
import { createScreenfullController } from "https://cdn.jsdelivr.net/npm/vue-screenfull/lib/browser.mjs";
const controller = createScreenfullController();
const target = document.querySelector("#player");
const button = document.querySelector("#toggle-fullscreen");
const toggle = () => controller.toggle(target);
button?.addEventListener("click", toggle);
async function dispose() {
button?.removeEventListener("click", toggle);
await controller.destroy();
}
// Call dispose() when the integration is disposed.
</script>
As a classic script:
<script src="https://cdn.jsdelivr.net/npm/vue-screenfull/lib/vue-screenfull.browser.min.js"></script>
<script>
const controller = VUE_SCREENFULL_BROWSER.createScreenfullController();
const target = document.querySelector("#player");
const button = document.querySelector("#toggle-fullscreen");
const toggle = () => controller.toggle(target);
button?.addEventListener("click", toggle);
function dispose() {
button?.removeEventListener("click", toggle);
return controller.destroy();
}
// Call dispose() when the integration is disposed.
</script>
Call request or toggle directly from a click, keyboard, or touch handler because browsers
normally require transient user activation. Retain the controller and call destroy() when the
integration is disposed so its listeners and any active CSS fallback are cleaned up.
The framework-neutral controller is useful for migration and non-component integrations:
import { createScreenfullController } from "vue-screenfull/browser";
const controller = createScreenfullController({ restoreFocus: true });
const onChange = (state) => console.log(state.isFullscreen, state.element);
controller.on("change", onChange);
await controller.request(document.querySelector("#map"));
controller.off("change", onChange);
await controller.destroy();
raw is a read-only diagnostic mapping of detected browser property and event names, or null. It
is not the recommended API. Each composable creates one controller and disposes it with its Vue
scope. Multiple controllers synchronize through the same document's native events. Importing the
package does not register listeners or touch the DOM.
Reactive callbacks can observe changes without duplicate component wiring:
useScreenfull({ onEnter: announce, onExit: announce, onError: report });
restoreFocus: true (the default) focuses the initiating element after exit where practical.
exitOnRouteChange listens for browser popstate. For router-specific navigation, call exit()
from the application's route hook instead.
The embedding page controls permission. A typical iframe is:
<iframe
src="https://example.com/player"
allow="fullscreen"
allowfullscreen
></iframe>
A Permissions Policy restriction or missing iframe permission can still reject the request. The
library returns IFRAME_PERMISSION_REQUIRED when it can associate a denial with an embedded
document. It cannot override the parent page's policy.
100dvh where supported.Support is detected at runtime. Current desktop releases of Chrome, Edge, Firefox, and Safari commonly expose the API. Chrome, Firefox, and Samsung Internet on Android and Safari on iPadOS also commonly expose it. iPhone Safari, Android and iOS WebViews, managed devices, and embedded documents may restrict arbitrary-element fullscreen. This policy identifies support targets; it does not guarantee support in every browser or operating-system release.
Native fullscreen can hide more browser UI, but the browser and operating system retain control. The CSS fallback only fills the visual viewport and never claims to hide system or browser chrome.
Imports are safe in Vite SSR, Nuxt 3, Node tests, and static generation. Outside a browser,
isEnabled is false, the status is unsupported, and actions return NOT_IN_BROWSER.
<script setup lang="ts">
import { useScreenfull } from "vue-screenfull";
const screenfull = useScreenfull(); // safe during Nuxt setup/SSR
</script>
<template>
<ClientOnly>
<button
type="button"
:disabled="!screenfull.isEnabled.value"
@click="screenfull.toggle()"
>
Toggle page fullscreen
</button>
</ClientOnly>
</template>
Templates automatically unwrap refs destructured in <script setup>. When accessing refs through
an object as shown in the example, use .value in script expressions.
vue-screenfull is an independent Vue 3 library inspired by screenfull's public API and
compatibility goals. It is not drop-in compatible and is not endorsed by screenfull's maintainers.
| screenfull concept | vue-screenfull equivalent |
|---|---|
screenfull.request(element, options) |
request(element, options) |
screenfull.exit() |
exit() |
screenfull.toggle(element, options) |
toggle(element, options) |
screenfull.isEnabled |
reactive isEnabled.value |
screenfull.isFullscreen |
reactive isFullscreen.value |
screenfull.element |
reactive fullscreenElement.value |
screenfull.on("change", fn) |
refs, callbacks, component events, or controller events |
screenfull.on("error", fn) |
reactive error, callbacks, or controller events |
Before:
import screenfull from "screenfull";
if (screenfull.isEnabled) await screenfull.toggle(element);
After:
import { useScreenfull } from "vue-screenfull";
const { isEnabled, toggle } = useScreenfull();
if (isEnabled.value) {
const result = await toggle(element);
if (!result.ok) console.error(result.error.message);
}
Key differences include reactive refs, automatic lifecycle cleanup, SSR-safe imports, structured
results and errors, optional pseudo-fullscreen, and Vue component and directive APIs. Controller
listeners receive typed state and errors instead of raw DOM events. Legacy onchange and onerror
aliases are not provided. The plugin is optional.
Root exports:
useScreenfull(options?), useScreenfullTarget(target, options?)Screenfull, vScreenfull, and the default plugincreateScreenfullController(options?)detectFullscreenApi(document) and resolveScreenfullTarget(target, document)The vue-screenfull/browser subpath exports createScreenfullController, detectFullscreenApi,
and only framework-neutral controller types.
Actions resolve to { ok, mode, element, error }. The mode value is native, fallback, or
none. Generated TypeDoc is published at
chengchuu.github.io/vue-screenfull/api/.
The deployed playground is available at chengchuu.github.io/vue-screenfull/playground/. It includes page, element, image-style, and video targets; explicit exit controls; diagnostics; invalid-target feedback; event history; and accessibility, iframe, mobile, and migration notes. Run it locally with:
npm run dev
Native fullscreen automation is intentionally not treated as universally reliable because browsers enforce user activation.
The project website is a Progressive Web App scoped to /vue-screenfull/. Its homepage, playground,
and API documentation share a generated manifest and a Google Workbox v7 service worker. Documents,
scripts, and styles use bounded network-first caches. This strategy normally prioritizes current
documentation without pairing fresh HTML with stale bundles. Local images and fonts use bounded
cache-first storage. The site uses a precached offline page only when the requested document is
unavailable from both the network and the runtime cache.
Installation uses the browser's native beforeinstallprompt flow when available. The site does not
open that prompt automatically. In browsers without a custom prompt, use the browser menu or, on
iOS and iPadOS Safari, Share → Add to Home Screen. Installing the website is separate from the
Fullscreen API and does not grant fullscreen capability.
Updated workers activate through the browser's normal lifecycle after existing controlled tabs close. The website does not force activation or reload an open page. The generated worker includes a final-artifact version marker, so deployable website changes can be detected without precaching unversioned bundles.
npm install
npm run typecheck
npm run lint
npm run format:check
npm run test
npm run build
npm run docs
npm run seo:validate
npm run pwa:validate
npm run preview
npm pack --dry-run
Normal npm run dev does not register the production worker. Build npm run docs and serve the
generated docs directory from localhost under /vue-screenfull/ for production-like PWA testing.
See guides/MANUAL_TESTING.md for the browser matrix and real-browser
strategy. The root entry produces lib/index.cjs.js, lib/index.esm.js, lib/index.mjs,
lib/vue-screenfull.min.js, lib/index.d.ts, lib/typing.d.ts, and lib/global.d.ts. The
framework-neutral entry produces lib/browser.cjs.js, lib/browser.esm.js, lib/browser.mjs,
lib/browser.d.ts, and lib/vue-screenfull.browser.min.js. JavaScript bundles include source maps.
Released under the MIT License. This independent project acknowledges
screenfull (MIT) for its public API and cross-browser
compatibility inspiration, and vue-fullscreen (MIT) as a Vue ecosystem reference. No endorsement
is implied.