Know when your users are really offline.

net-watch checks whether the browser can reach your server, measures how fast the connection is, and hands you one state object. It works with React, Vue, Angular or plain JavaScript.

Under 3 kB gzipped, no dependencies. Runs in Chrome, Edge, Firefox and Safari. The panel on this page is the library measuring your connection right now, and the line at the very top of the page changes colour with it.

Online
checking this connection
--
quality unknown
excellent
under 150 ms
good
150 to 400 ms
fair
400 to 1400 ms
poor
1400 ms and up

Last minute. Dots are your requests; the line is the smoothed latency net-watch reports.

download
unknown
effective type
unknown
link type
unknown
data saver
off

To test for real, switch on airplane mode or use DevTools, Network, Offline. The simulate button only fires the browser's offline event: net-watch then probes, finds the connection works, and recovers in about two seconds.

navigator.onLine only knows that a network interface is up

The browser reports "online" as soon as the device has a network interface. It does not know whether the router behind it has internet, whether the Wi-Fi is waiting for a sign-in page, or whether your own server can be reached.

Apps that trust it keep showing a happy state while every request fails. Users stare at a spinner, retry by hand, and blame your app. The reverse direction is reliable: when the browser says it is offline, it is. net-watch starts from that fact and checks the rest.

Hooks that only wrap the online and offline events inherit the same blind spot. Polling an endpoint every few seconds fixes it, but spends battery and data on every visitor, all day, to answer a question that only matters when something goes wrong.

SituationBrowser saysnet-watch says
Wi-Fi is connected but the router has no internetonlineoffline
Hotel or airport Wi-Fi is waiting for a sign-in pageonlineoffline
A VPN, Docker or virtual adapter is up and Wi-Fi is offonlineoffline
Your server is down or blocked by a firewallonlineoffline
You are in a tunnel: connected, but every request takes secondsonlineonline, quality poor
Airplane modeofflineoffline

It asks the network only when something looks wrong

net-watch collects evidence from sources that cost nothing and sends a request of its own only when those sources disagree or go quiet. While the app is online and idle it sends nothing beyond a check at startup and one when the tab comes back into view, unless you opt in to a heartbeat.

The result is one immutable state object. It changes only when a value changes after rounding, so a stream of similar measurements does not re-render your UI.

See the full flow, with diagrams

  1. The browser's own signals

    The online and offline events, the tab becoming visible again, and a page restored from the back and forward cache. A report of "offline" is trusted at once.

  2. Your own requests

    Wrap fetch once. Every response proves the network works and becomes a latency sample. A network error raises a suspicion.

  3. A probe, when needed

    A tiny HEAD request to your ping endpoint settles a suspicion. While offline it retries after 2 seconds, then 4, 8 and so on up to 30, and stops as soon as the connection is back.

Latency, quality and download speed from what the browser already measures

Every probe and every request you send yields a time to first byte. net-watch smooths those samples, rounds them to 50 ms, and sorts the result into four quality bands: excellent under 150 ms, good under 400, fair under 1400, and poor above. A quality level improves only after latency falls well below a boundary, so the label does not flicker.

Download speed comes from the browser's resource timing of large responses you already loaded. There are no test files to download and no extra data for your users to pay for. If you want a number on demand, load a large file (50 kB or more, from your own origin or with a Timing-Allow-Origin header) that is not served from cache, and the value updates.

Some things are not available to web pages in any browser, such as Wi-Fi signal strength. net-watch leaves those fields out instead of guessing, and a value the browser does not expose is simply undefined.

How the numbers are computed

State fieldWhere you get it
online, sinceEvery browser
rtt, qualityEvery browser, once one request or probe has finished
downlinkChrome, Edge, Firefox, Safari 16.4 and later
effectiveType, saveDataChrome and Edge
type (wifi, cellular)Chrome on Android and ChromeOS

One state object in whatever framework you use

The core has no framework code. Each adapter is about ten lines that turn the same state into a React hook, a Vue computed value or an Angular signal, and weighs under 400 bytes. Plain JavaScript works the same way through subscribe.

import { useNetworkStatus } from '@net-watch/react'

export function ConnectionBadge() {
  const { online, quality, rtt } = useNetworkStatus()

  if (!online) return <span>Offline</span>
  return <span>{quality}{rtt ? `, ${rtt} ms` : ''}</span>
}

Give your AI assistant the context it needs

Assistants write better integration code when they read the real documentation instead of guessing from memory. This site is built for that: every page is available as Markdown, and two index files list everything.

  • /llms.txt lists every page with its description.
  • /llms-full.txt contains all the documentation in one file.
  • Add .md to any docs URL to get that page as Markdown.

Paste the prompt on the right into your assistant to add net-watch to a project. More prompts and a rules snippet for AGENTS.md

Add net-watch to my app
Add net-watch to this project.

1. Read https://YOUR-SITE/llms-full.txt (the net-watch docs in one file) before writing any code, and follow it instead of your memory.
2. Detect the framework. Install @net-watch/core and the matching adapter: @net-watch/react, @net-watch/vue or @net-watch/angular.
3. Create one watcher in its own module: createNetworkWatcher({ pingUrl: '/api/ping' }). Never create a watcher per component.
4. Add a /api/ping route to the backend that answers GET and HEAD with 204 and Cache-Control: no-store.
5. Replace the app's fetch with watchFetch(watcher) from @net-watch/core/fetch, so real requests are measured.
6. Mount createBanner(watcher, { slowText: 'Slow connection' }) from @net-watch/core/banner inside an effect, and destroy it on cleanup.
7. Show connection state with the framework adapter (useNetworkStatus or injectNetworkStatus).

Do not poll navigator.onLine yourself. Do not invent options: only use what the docs list. Run the type checker when you are done.

Small, dependency-free, and tested in three engines

Size
The core is under 3 kB gzipped. The banner adds about 2 kB and watchFetch about 0.3 kB. No runtime dependencies.
Browsers
Chrome and Edge 90, Firefox 90, Safari and iOS 15 or newer. Every test runs in real Chromium, Firefox and WebKit.
Server rendering
Safe to import and create without a window. Nothing runs on the server, and React gets a proper server snapshot.
Types
Written in TypeScript with strict settings. Every field of the state is typed, and unknown values are explicit.

Add it in the time it takes to read this page.

npm i @net-watch/core
Read the quick start