network-guard-engine

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.
957 1
Install
npm install network-guard-engine
Latest Version:1.1.1
Vue:3
License:MIT
Last Updated:Jun 16, 2026
Links: GitHub  ·  npm
Maintainer: code-plate

network-guard-engine

npm version license types

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

  • 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, plus effectiveType, downlink, rtt, saveData, and transport type from the Network Information API
  • Active reachability probe — captive-portal detection via redirect: "manual" fetch; real internet yields opaque, portal redirect yields opaqueredirect
  • 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-agnostic core / handlers

Installation

npm install network-guard-engine
# or
pnpm add network-guard-engine
# or
yarn add network-guard-engine

Framework 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 resolver that 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 react

useNetworkStatus

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" and aria-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 assessment

createNetworkMonitor(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" for quality, effectiveType, downlink, and rtt — 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

Related Packages

@net-watch/vue

Vue 3 composable useNetworkStatus() for real online/offline detection and connec...

0 3
@symbiote-native/network

expo-network wrapped for SymbioteNative — one framework-agnostic core, built onc...

1,889 72
@deffector/agent

DevTools for effector: every user action as one transaction, time travel, state...

919 16
@esmx/core

A high-performance microfrontend framework supporting Vue, React, Preact, Solid,...

8,645 680

Version History

Version Released Vue License
1.1.1 >=3.0.0 MIT
1.1.0 >=3.0.0 MIT
1.0.3 MIT
1.0.2 MIT