EngineBrowser
EngineBrowser exposes browser detection plus clipboard, interaction, media,
speech, and network helpers.
The public nextjs-engine entrypoint exports the SSR-safe facade from
core/EngineBrowserSafe. The lower-level detector remains internal-compatible,
but application code should import from the package/root engine entrypoint.
SSR contract
Detection is safe during server rendering:
EngineBrowser.name
// "server"
EngineBrowser.info
// server-safe BrowserInfoBrowser-only subsystems do not synchronously touch window, document,
navigator, screen, or other DOM globals when no DOM exists.
Server/default results are:
Call | Server result |
|---|---|
clipboard writes |
|
clipboard reads |
|
interaction capability calls |
|
camera/microphone/screen capture |
|
speech recognition |
|
|
|
|
|
network status |
|
EngineBrowser.speech.speak() is the one deliberate exception to the
"default-value" pattern: its return type is Promise<void>, so an unavailable
speech-synthesis API returns a rejected promise with
Speech synthesis not supported. It does not synchronously throw while merely
evaluating browser globals.
Detection
import { EngineBrowser } from "nextjs-engine"
EngineBrowser.is.chrome
EngineBrowser.is.firefox
EngineBrowser.is.safari
EngineBrowser.is.edge
EngineBrowser.is.opera
EngineBrowser.is.brave
EngineBrowser.is.osmium
EngineBrowser.is.chromium
EngineBrowser.is.mobile
EngineBrowser.is.tablet
EngineBrowser.is.desktop
EngineBrowser.name
EngineBrowser.engine
EngineBrowser.versionFeature flags are available under supports:
EngineBrowser.supports.viewTransitions
EngineBrowser.supports.containerQueries
EngineBrowser.supports.cssHas
EngineBrowser.supports.cssNesting
EngineBrowser.supports.cssLayer
EngineBrowser.supports.webgl2
EngineBrowser.supports.clipboard
EngineBrowser.supports.camera
EngineBrowser.supports.screenCapture
EngineBrowser.supports.speechSynthesis
EngineBrowser.supports.speechRecognitionThe public facade uses condition-form CSS.supports(...) checks for :has()
and nesting instead of treating selectors as CSS property names. Cascade-layer
support is based on the browser's CSSLayerBlockRule exposure.
Detection is cached. Call EngineBrowser.invalidate() in tests or after
deliberately changing the emulated browser environment.
Conditional execution
EngineBrowser.run({
safari: () => applySafariFix(),
firefox: () => applyFirefoxFix(),
default: () => applyDefault(),
})
const value = EngineBrowser.pick({
mobile: "compact",
default: "desktop",
})prefixed(property, value) returns the normal declaration and adds the WebKit
form for the small set of properties that still need it.
Clipboard
await EngineBrowser.clipboard.copy("Hello")
await EngineBrowser.clipboard.copyHtml(
"<strong>Hello</strong>",
"Hello",
)
const text = await EngineBrowser.clipboard.paste()
const items = await EngineBrowser.clipboard.read()
const mayRead = await EngineBrowser.clipboard.canRead()
const mayWrite = await EngineBrowser.clipboard.canWrite()Clipboard helpers catch permission/API failures and return false, null, or
[]. Plain-text copying retains the legacy execCommand("copy") fallback.
Interactions
await EngineBrowser.interact.share({
title: "Next.js Engine",
url: location.href,
})
const notification = await EngineBrowser.interact.notify("Done", {
body: "Export complete",
})
EngineBrowser.interact.vibrate([100, 50, 100])
const files = await EngineBrowser.interact.pickFile({
accept: "image/*",
multiple: true,
})
EngineBrowser.interact.download(
"export.json",
JSON.stringify(data),
"application/json",
)
await EngineBrowser.interact.fullscreen()
await EngineBrowser.interact.exitFullscreen()
const lock = await EngineBrowser.interact.wakeLock()
await lock?.release()
const position = await EngineBrowser.interact.location({
enableHighAccuracy: true,
})
await EngineBrowser.interact.lockOrientation("portrait")
await EngineBrowser.interact.badge(3)
await EngineBrowser.interact.clearBadge()Unavailable APIs and rejected permission prompts resolve to their documented false/null/no-op result rather than leaking browser-global errors into SSR.
Media
const camera = await EngineBrowser.media.camera({
facing: "environment",
width: 1920,
height: 1080,
})
const microphone = await EngineBrowser.media.microphone()
const display = await EngineBrowser.media.screen()
EngineBrowser.media.stop(camera!)Always stop capture streams when finished. media.stop() is defensive: passing
an already-ended or invalid stream does not turn cleanup into an application
error.
Speech synthesis
await EngineBrowser.speech.speak("Hello", {
lang: "en-US",
rate: 1.1,
pitch: 1,
volume: 1,
})
EngineBrowser.speech.stopSpeaking()
EngineBrowser.speech.isSpeaking()Ranges are normalized before assigning the native utterance:
Option | Range |
|---|---|
|
|
|
|
|
|
0 is a valid pitch/volume value and is not discarded by truthiness checks.
Starting a new speak() call settles/cancels the previous engine-owned
utterance first. Calling stopSpeaking() also settles the engine-owned promise
before asking the browser to cancel playback, avoiding promises that hang after
a cancellation event.
Speech recognition
const text = await EngineBrowser.speech.listen(
{
lang: "en-US",
interim: true,
maxSilence: 4,
},
(partial) => {
console.log(partial)
},
)maxSilence is implemented by the facade. The timer starts with recognition and
is reset as speech/results arrive. On timeout the recognition session is
stopped and the transcript collected so far is returned.
Only one engine-owned recognition session is active at a time. Starting a new
listen() call aborts and resolves the previous one with null. This prevents
an older onend/onerror callback from clearing or resolving a newer session.
stopListening() aborts the active session and resolves its pending call with
null.
EngineBrowser.speech.stopListening()Voices
const voices = EngineBrowser.speech.voices()
const japanese = voices.find((voice) => voice.lang === "ja-JP")Some browsers populate synthesis voices asynchronously, so an early call can legitimately return an empty array.
Network
const status = EngineBrowser.network.status()
const unsubscribe = EngineBrowser.network.onchange((next) => {
console.log(next.online, next.type)
})
// later
unsubscribe()NetworkStatus.type is normalized to the documented NetworkType union.
Unknown browser-specific strings become "other" while offline unknowns become
"none".
Known values include:
wifi
ethernet
4g
3g
2g
slow-2g
bluetooth
wimax
other
none
unknownReact hook
import { useBrowser } from "nextjs-engine"
function BrowserAware() {
const browser = useBrowser()
if (browser.is.safari) return <SafariVariant />
return <StandardVariant />
}The hook keeps the exact server snapshot on the client's first render, then updates after mount. This avoids changing feature flags during hydration merely because DOM globals became available.