network-guard-engine
v1.1.1
Published
Zero-dependency network status monitor — detects offline, slow connections, captive portals and VPN/proxy, with pluggable handlers and React/Vue/Svelte/Solid/Next adapters. Framework-agnostic; all text is user-supplied.
Maintainers
Readme
network-guard-engine
Zero-dependency network status monitor with rich connection signals and pluggable notification handlers.
Works in React, Next.js, Vue, Svelte, Solid, and vanilla JavaScript — bring your own notification system or use none at all.
Table of Contents
- Features
- Installation
- Quick Start
- Vanilla JavaScript
- Connection Quality & Detection
- React Integration
- Next.js Integration
- Vue Integration
- Svelte Integration
- Solid Integration
- Built-in Handlers
- API Reference
- Browser Support
- TypeScript
Features
- Zero runtime dependencies — core has no dependencies
- Rich connection signals — beyond online/offline, detect slow connections, captive portals ("connected, no internet"), and VPN/proxy (best-effort)
- Connection quality —
good/slow/captive/offline, pluseffectiveType,downlink,rtt,saveData, and transporttypefrom the Network Information API - Active reachability probe — captive-portal detection via
redirect: "manual"fetch; real internet yieldsopaque, portal redirect yieldsopaqueredirect - VPN/proxy heuristic — timezone mismatch, abnormal RTT, optional WebRTC probe, or your own resolver
- SSR-safe — all browser APIs are properly guarded
- Localization-ready — every surfaced string comes from
messages; built-in defaults are English and fully overridable (no hard-coded UI text) - Pluggable architecture — observer
subscribe()API + use any notification system - Revalidate on focus / visibility — re-checks connection when the tab returns
- Debounced events — prevents flicker on unstable connections; duplicate consecutive events suppressed
- Fully typed — built with TypeScript, strict settings
- Tree-shakeable — framework integrations are isolated entry points
- Framework adapters —
react,next,vue,svelte,solid, plus framework-agnosticcore/handlers
Installation
npm install network-guard-engine
# or
pnpm add network-guard-engine
# or
yarn add network-guard-engineFramework adapters are optional peer dependencies — install only what you use
(react, vue, svelte, or solid-js). The core and handlers entry points
need nothing.
Quick Start
import { createNetworkMonitor } from "network-guard-engine";
const monitor = createNetworkMonitor({
onOnline: ({ message }) => console.log(message),
onOffline: ({ message }) => console.warn(message),
});
// Stop when done
monitor.stop();Vanilla JavaScript
createNetworkMonitor(options?)
The simplest way to get started. Creates a monitor and starts it immediately.
import { createNetworkMonitor } from "network-guard-engine";
const monitor = createNetworkMonitor({
messages: {
online: "Connection restored ✓",
offline: "Connection lost ✗",
offlineOnMount: "No network connection detected",
},
debounce: 800, // wait 800ms before firing (default: 500)
checkOnMount: true, // fire offline handler immediately if offline (default: true)
onOnline: ({ message, timestamp, isInitial }) => {
console.log(`[${timestamp.toLocaleTimeString()}] ${message}`);
},
onOffline: ({ message, isInitial }) => {
if (!isInitial) console.warn("Lost connection:", message);
},
});
// Clean up when done (e.g. on page unload or SPA route change)
monitor.stop();new NetworkMonitor(options?)
Use the class directly when you need more control, such as starting and stopping imperatively.
import { NetworkMonitor } from "network-guard-engine";
const monitor = new NetworkMonitor({
onOnline: ({ message }) => showToast(message, "success"),
onOffline: ({ message }) => showToast(message, "error"),
});
// Start manually (e.g. after user logs in)
monitor.start();
// Check status at any time
console.log(monitor.running); // true
console.log(monitor.status); // "online" | "offline" | null
// Stop and restart
monitor.stop();
monitor.start();Connection Quality & Detection
Beyond binary online/offline, the monitor surfaces a qualitative assessment and
rich connection details. Everything degrades gracefully to "unknown"/null
where the platform doesn't expose the signal (e.g. the Network Information API
is unavailable in Safari and Firefox).
import { createNetworkMonitor } from "network-guard-engine";
const monitor = createNetworkMonitor({
// All strings are user-supplied; defaults are English.
messages: {
offline: "You are offline.",
slow: "Your connection is slow.",
captive: "Connected, but no internet access.",
vpn: "A VPN or proxy was detected.",
},
onOffline: ({ message }) => banner(message),
onSlow: ({ connection }) => banner(`Slow (${connection.effectiveType})`),
onCaptive: ({ message }) => banner(message),
onVpn: ({ message }) => banner(message),
// Optional: active reachability probe (captive-portal detection).
reachability: {
enabled: true,
url: "https://www.gstatic.com/generate_204", // your own endpoint is better
timeoutMs: 5000,
intervalMs: 0, // >0 to poll while online
},
// Optional: best-effort VPN/proxy heuristic.
vpn: {
enabled: true,
expectedTimezone: "Europe/Berlin", // mismatch raises likelihood
suspiciousRttMs: 400,
useWebRtc: false, // opt-in; privacy-sensitive
// resolver: async () => callMyBackend(), // authoritative override
},
});ConnectionInfo snapshot
monitor.getConnection() and payload.connection return a frozen snapshot:
| Field | Type | Notes |
|-----------------|-----------------------------------------------------------|-----------------|
| status | "online" \| "offline" | Coarse state |
| quality | "good" \| "slow" \| "captive" \| "offline" \| "unknown" | Qualitative |
| type | "wifi" \| "cellular" \| "ethernet" \| … | Transport |
| effectiveType | "slow-2g" \| "2g" \| "3g" \| "4g" \| "unknown" | NIC class |
| downlink | number \| null | Mbit/s estimate |
| rtt | number \| null | ms estimate |
| saveData | boolean | Save-Data header|
| isSlow | boolean | Derived |
| vpn | "likely" \| "possible" \| "unlikely" \| "unknown" | Heuristic |
| reachable | boolean \| null | Last probe result|
VPN detection is heuristic. Browsers expose no authoritative VPN flag, so the result is a confidence level. For certainty, supply a
resolverthat checks the client IP against known datacenter/VPN ranges on your backend.
Framework-agnostic observer API
No framework? Subscribe directly:
const monitor = createNetworkMonitor();
const unsubscribe = monitor.subscribe((e) => {
render(e.quality, e.message);
});
// later: unsubscribe();React Integration
# React is a peer dependency — install if you haven't already
npm install reactuseNetworkStatus
The simplest React hook. Returns the current network status and quality as reactive values.
import { useNetworkStatus } from "network-guard-engine/react";
function App() {
const { isOnline, isOffline, isSlow, isCaptive, quality, connection, lastChanged } =
useNetworkStatus({
reachability: { enabled: true },
vpn: { enabled: true, expectedTimezone: "Asia/Tehran" },
});
return (
<>
{isOffline && <div className="banner">Offline</div>}
{isSlow && <div className="banner warn">Slow ({connection.effectiveType})</div>}
{isCaptive && <div className="banner warn">Connected, no internet</div>}
<main>{/* your app */}</main>
</>
);
}Return values:
| Field | Type | Description |
|---------------|-----------------------------------------------------------|----------------------------------------|
| status | "online" \| "offline" | Current network status |
| isOnline | boolean | Shorthand for status === "online" |
| isOffline | boolean | Shorthand for status === "offline" |
| quality | "good" \| "slow" \| "captive" \| "offline" \| "unknown" | Qualitative assessment |
| isSlow | boolean | Shorthand for quality === "slow" |
| isCaptive | boolean | Shorthand for quality === "captive" |
| vpn | "likely" \| "possible" \| "unlikely" \| "unknown" | VPN/proxy likelihood |
| connection | ConnectionInfo | Frozen connection snapshot |
| lastChanged | Date \| null | Timestamp of the last status change |
useNetworkMonitor
A lower-level hook that provides imperative start / stop / refresh control alongside the reactive status values.
import { useNetworkMonitor } from "network-guard-engine/react";
function MonitorPanel() {
const { isOnline, isOffline, isMonitoring, start, stop, refresh, lastChanged } =
useNetworkMonitor({
debounce: 800,
messages: { offline: "Connection interrupted." },
});
return (
<div>
<p>Status: {isOnline ? "🟢 Online" : "🔴 Offline"}</p>
{lastChanged && <p>Since: {lastChanged.toLocaleTimeString()}</p>}
<button onClick={isMonitoring ? stop : start}>
{isMonitoring ? "Stop monitoring" : "Start monitoring"}
</button>
<button onClick={refresh}>Refresh</button>
</div>
);
}Options:
| Option | Type | Default | Description |
|----------------|-------------------|----------|--------------------------------------------------|
| debounce | number | 500 | Milliseconds to wait before firing a handler |
| checkOnMount | boolean | true | Fire offline handler on mount if already offline |
| messages | NetworkMessages | built-in | Custom message strings |
| reachability | ReachabilityConfig | — | Active probe config |
| vpn | VpnDetectionConfig | — | VPN heuristic config |
Return values (extends UseNetworkStatusReturn):
| Field | Type | Description |
|----------------|--------------|------------------------------------------------|
| isMonitoring | boolean | true while the monitor is actively listening |
| start | () => void | Start monitoring (no-op if already running) |
| stop | () => void | Stop monitoring and cancel pending debounce |
| refresh | () => void | Force an immediate re-evaluation |
NetworkProvider
A headless provider component that fires your own notification callbacks on network changes. No built-in UI — bring your own toast library, Redux action, or Zustand setter.
import { NetworkProvider } from "network-guard-engine/react";
import { toast } from "sonner";
let offlineToastId: string | number | null = null;
export function Providers({ children }: { children: React.ReactNode }) {
return (
<NetworkProvider
messages={{
online: "Back online!",
offline: "Connection lost. Please check your network.",
slow: "Your connection is slow.",
captive: "Connected, but no internet access.",
}}
onOnline={({ message }) => {
if (offlineToastId !== null) toast.dismiss(offlineToastId);
toast.success(message, { duration: 3000 });
offlineToastId = null;
}}
onOffline={({ message }) => {
if (offlineToastId !== null) toast.dismiss(offlineToastId);
offlineToastId = toast.error(message, { duration: Infinity });
}}
onSlow={({ message }) => toast.warning(message, { duration: 4000 })}
onCaptive={({ message }) => toast.error(message, { duration: Infinity })}
reachability={{ enabled: true }}
>
{children}
</NetworkProvider>
);
}Props:
| Prop | Type | Default | Description |
|-------------------|-------------------|----------|------------------------------------------------|
| children | React.ReactNode | — | Required. Rendered as-is. |
| onOnline | NetworkHandler | — | Called when connection is restored |
| onOffline | NetworkHandler | — | Called when connection is lost |
| onSlow | NetworkHandler | — | Called when connection is slow |
| onCaptive | NetworkHandler | — | Called on captive portal detection |
| onVpn | NetworkHandler | — | Called when VPN/proxy is likely |
| onChange | NetworkHandler | — | Called on every network event |
| debounce | number | 500 | Milliseconds to debounce before firing |
| checkOnMount | boolean | true | Fire onOffline on mount if already offline |
| messages | NetworkMessages | built-in | Custom message strings |
| reachability | ReachabilityConfig | — | Active probe config |
| vpn | VpnDetectionConfig | — | VPN heuristic config |
Spread with built-in handlers:
import { consoleHandler } from "network-guard-engine/handlers";
<NetworkProvider {...consoleHandler({ offlineLevel: "error" })}>
{children}
</NetworkProvider>With Zustand:
import { useNetworkStore } from "@/stores/network";
function AppShell({ children }: { children: React.ReactNode }) {
const setOnline = useNetworkStore((s) => s.setOnline);
const setOffline = useNetworkStore((s) => s.setOffline);
return (
<NetworkProvider onOnline={setOnline} onOffline={setOffline}>
{children}
</NetworkProvider>
);
}Next.js Integration
The /next entry re-exports everything from /react with a "use client" directive already prepended, so you don't need to add it yourself.
// app/providers.tsx
import { NetworkProvider } from "network-guard-engine/next";
import { toast } from "sonner";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<NetworkProvider
onOnline={({ message }) => toast.success(message)}
onOffline={({ message }) => toast.error(message, { duration: Infinity })}
onSlow={({ message }) => toast.warning(message, { duration: 4000 })}
onCaptive={({ message }) => toast.error(message, { duration: Infinity })}
reachability={{ enabled: true }}
>
{children}
</NetworkProvider>
);
}// app/layout.tsx
import { Providers } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Hooks are available from the /next entry too:
"use client";
import { useNetworkStatus } from "network-guard-engine/next";Vue Integration
Import from network-guard-engine/vue (peer dependency: vue >= 3).
<script setup lang="ts">
import { useNetworkStatus } from "network-guard-engine/vue";
const { isOffline, isSlow, isCaptive, quality, connection } = useNetworkStatus({
reachability: { enabled: true },
messages: { offline: "You are offline." },
});
</script>
<template>
<div v-if="isOffline" class="banner">Offline</div>
<div v-else-if="isCaptive" class="banner warn">Connected, no internet</div>
<div v-else-if="isSlow" class="banner warn">Slow ({{ connection.effectiveType }})</div>
</template>The composable auto-stops the monitor on scope dispose. It exposes
status, isOnline, isOffline, quality, isSlow, isCaptive, vpn,
connection, lastChanged, refresh(), and stop().
Svelte Integration
Import from network-guard-engine/svelte (peer dependency: svelte >= 4).
Returns a readable store; the monitor runs while the store has subscribers.
<script lang="ts">
import { networkStatus } from "network-guard-engine/svelte";
const net = networkStatus({ reachability: { enabled: true } });
</script>
{#if $net.isOffline}
<div class="banner">Offline</div>
{:else if $net.isCaptive}
<div class="banner warn">Connected, no internet</div>
{:else if $net.isSlow}
<div class="banner warn">Slow connection</div>
{/if}Solid Integration
Import from network-guard-engine/solid (peer dependency: solid-js >= 1).
Cleans up automatically via onCleanup.
import { Show } from "solid-js";
import { useNetworkStatus } from "network-guard-engine/solid";
function Banner() {
const { isOffline, isSlow, isCaptive } = useNetworkStatus({
reachability: { enabled: true },
});
return (
<>
<Show when={isOffline()}><div class="banner">Offline</div></Show>
<Show when={isCaptive()}><div class="banner warn">Connected, no internet</div></Show>
<Show when={isSlow()}><div class="banner warn">Slow</div></Show>
</>
);
}Accessors returned: status, isOnline, isOffline, quality, isSlow,
isCaptive, vpn, connection, lastChanged, plus refresh().
Built-in Handlers
Import from network-guard-engine/handlers (or from the main entry for vanilla usage).
consoleHandler
Logs network status changes to the browser or Node.js console.
import { createNetworkMonitor } from "network-guard-engine";
import { consoleHandler } from "network-guard-engine/handlers";
createNetworkMonitor({
...consoleHandler({
offlineLevel: "error", // "log" | "warn" | "error" (default: "warn")
onlineLevel: "info", // "log" | "info" (default: "log")
showTimestamp: true, // (default: true)
messages: {
online: "Connected",
offline: "Disconnected",
},
}),
});Output example:
[14:32:07] Disconnected ← offline (logged via console.error)
[14:32:45] Connected ← online (logged via console.info)alertHandler
Shows a native window.alert() dialog on each status change.
Best for quick debugging or environments without a notification system. Not recommended for production.
import { createNetworkMonitor } from "network-guard-engine";
import { alertHandler } from "network-guard-engine/handlers";
createNetworkMonitor({
...alertHandler({
messages: {
online: "You're back online.",
offline: "You lost your connection.",
},
}),
});customHandler
The recommended adapter for integrating any external toast or notification library. Receives a clean message string plus the full NetworkEventPayload.
import { createNetworkMonitor } from "network-guard-engine";
import { customHandler } from "network-guard-engine/handlers";
import { toast } from "sonner";
let offlineToastId: string | number | null = null;
createNetworkMonitor({
...customHandler({
messages: {
online: "Back online!",
offline: "Connection lost. Please check your network.",
offlineOnMount: "No internet connection detected.",
slow: "Your connection is slow.",
captive: "Connected, but no internet access.",
},
onOnline: (message) => {
if (offlineToastId !== null) toast.dismiss(offlineToastId);
toast.success(message, { duration: 3000 });
offlineToastId = null;
},
onOffline: (message) => {
if (offlineToastId !== null) toast.dismiss(offlineToastId);
offlineToastId = toast.error(message, { duration: Infinity });
},
}),
});Full payload access (second argument):
customHandler({
onOffline: (message, payload) => {
// payload.isInitial — true only on the first mount check
if (!payload.isInitial) Sentry.captureMessage("User went offline");
showBanner(message);
},
});With React Hot Toast:
import toast from "react-hot-toast";
createNetworkMonitor({
...customHandler({
onOnline: (message) => toast.success(message),
onOffline: (message) => toast.error(message, { duration: Infinity }),
}),
});domBannerHandler
Injects a fixed, animated banner at the top of the page — no React, no external libraries required.
- Created lazily on the first event and reused for subsequent events.
- Slides in/out using CSS transitions.
- Fully accessible: sets
role="alert"andaria-live="assertive".
import { createNetworkMonitor } from "network-guard-engine";
import { domBannerHandler } from "network-guard-engine/handlers";
createNetworkMonitor({
...domBannerHandler(),
});With custom options:
createNetworkMonitor({
...domBannerHandler({
container: "#app-root", // CSS selector or HTMLElement (default: document.body)
onlineDuration: 2500, // ms before hiding the "back online" banner (default: 3000)
messages: {
online: "Reconnected!",
offline: "No connection",
offlineOnMount: "You appear to be offline.",
},
offlineStyle: {
background: "#7f1d1d",
letterSpacing: "0.05em",
},
onlineStyle: {
background: "#14532d",
},
}),
});Options:
| Option | Type | Default | Description |
|------------------|--------------------------------|-----------------|-----------------------------------------------------------------------------|
| messages | NetworkMessages | built-in | Custom text for each status |
| container | string \| HTMLElement | document.body | Where to mount the banner element |
| offlineStyle | Partial<CSSStyleDeclaration> | — | Additional styles applied while offline |
| onlineStyle | Partial<CSSStyleDeclaration> | — | Additional styles applied while online |
| onlineDuration | number | 3000 | How long (ms) the "back online" banner stays visible. Set 0 to keep it until next change. |
API Reference
NetworkEventPayload
Delivered to every handler. All fields are readonly — the object is frozen.
type NetworkEventPayload = {
readonly status: "online" | "offline";
readonly quality: "good" | "slow" | "captive" | "offline" | "unknown";
readonly message: string; // resolved human-readable message
readonly isInitial: boolean; // true only on the first mount check
readonly timestamp: Date; // when this event was fired
readonly connection: ConnectionInfo; // frozen connection snapshot
};NetworkMessages
All fields are optional and fall back to built-in defaults.
type NetworkMessages = {
online?: string; // default: "Back online"
offline?: string; // default: "You are offline. Please check your connection."
offlineOnMount?: string; // default: "No internet connection. Please check your network."
slow?: string; // default: "Your connection is slow."
captive?: string; // default: "Connected, but no internet access."
vpn?: string; // default: "A VPN or proxy connection was detected."
};NetworkMonitor class
new NetworkMonitor(options?: NetworkMonitorOptions): NetworkMonitor
monitor.start(): this // start listening (chainable)
monitor.stop(): this // stop and cancel pending debounce
monitor.refresh(): this // force immediate re-evaluation
monitor.subscribe(fn): () => void // observer pattern; returns unsubscribe
monitor.getConnection(): ConnectionInfo // frozen snapshot of current state
monitor.running: boolean // true while actively listening
monitor.status: "online" | "offline" | null // last known status (null before start)
monitor.quality: ConnectionQuality // last qualitative assessmentcreateNetworkMonitor(options?)
createNetworkMonitor(options?: NetworkMonitorOptions): NetworkMonitor
// Equivalent to: new NetworkMonitor(options).start()NetworkMonitorOptions
| Option | Type | Default | Description |
|----------------------|-----------------------|----------|----------------------------------------------------------|
| debounce | number | 500 | Milliseconds to wait before firing after a status change |
| checkOnMount | boolean | true | Fire onOffline immediately on start if already offline |
| revalidateOnFocus | boolean | true | Re-evaluate on tab focus / visibility |
| onOnline | NetworkHandler | — | Called when connection is restored |
| onOffline | NetworkHandler | — | Called when connection is lost |
| onSlow | NetworkHandler | — | Called when connection quality is slow |
| onCaptive | NetworkHandler | — | Called on captive portal detection |
| onVpn | NetworkHandler | — | Called when VPN/proxy is likely |
| onChange | NetworkHandler | — | Called on every network event |
| messages | NetworkMessages | built-in | Custom message strings |
| reachability | ReachabilityConfig | — | Active probe config |
| vpn | VpnDetectionConfig | — | VPN heuristic config |
ReachabilityConfig
| Option | Type | Default | Description |
|--------------|----------|------------------------------------------|--------------------------------------|
| enabled | boolean| false | Enable captive-portal probe |
| url | string | https://www.gstatic.com/generate_204 | Probe endpoint (own endpoint recommended) |
| timeoutMs | number | 5000 | Probe timeout |
| intervalMs | number | 0 | Poll interval while online; 0 = one-shot |
VpnDetectionConfig
| Option | Type | Default | Description |
|---------------------|---------------------------------------------|---------|----------------------------------------------|
| enabled | boolean | false | Enable VPN heuristic |
| expectedTimezone | string | — | e.g. "Asia/Tehran" — mismatch raises signal|
| suspiciousRttMs | number | 400 | RTT threshold raising a signal |
| useWebRtc | boolean | false | Opt-in WebRTC host candidate probe |
| resolver | () => Promise<VpnLikelihood \| undefined> | — | Custom authoritative resolver |
Browser Support
| Browser | Support | |---------------|----------------------------------------------------------| | Chrome 66+ | ✅ | | Firefox 60+ | ✅ | | Safari 12.1+ | ✅ | | Edge 79+ | ✅ | | Node.js / SSR | ✅ (safe — all browser APIs are guarded) |
The library uses window.addEventListener("online" / "offline") and navigator.onLine, which have broad browser support. The core module is SSR-safe: every window and navigator access is guarded, so you can safely import and instantiate the monitor in a server environment — it simply becomes a no-op until .start() is called in the browser.
The Network Information API (
navigator.connection) is currently available in Chrome and Edge. Safari and Firefox always return"unknown"forquality,effectiveType,downlink, andrtt— the monitor degrades gracefully.
TypeScript
All types are exported from the main entry point:
import type {
NetworkStatus,
ConnectionQuality,
ConnectionInfo,
NetworkMessages,
NetworkEventPayload,
NetworkHandler,
NetworkMonitorOptions,
ReachabilityConfig,
VpnDetectionConfig,
VpnLikelihood,
NetworkProviderProps,
UseNetworkStatusReturn,
UseNetworkMonitorReturn,
AlertHandlerOptions,
ConsoleHandlerOptions,
CustomHandlerOptions,
DomBannerHandlerOptions,
} from "network-guard-engine";License
MIT — MJavadSF
