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.
npm i @net-watch/coreUnder 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.
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.
| Situation | Browser says | net-watch says |
|---|---|---|
| Wi-Fi is connected but the router has no internet | online | offline |
| Hotel or airport Wi-Fi is waiting for a sign-in page | online | offline |
| A VPN, Docker or virtual adapter is up and Wi-Fi is off | online | offline |
| Your server is down or blocked by a firewall | online | offline |
| You are in a tunnel: connected, but every request takes seconds | online | online, quality poor |
| Airplane mode | offline | offline |
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.
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.
Your own requests
Wrap
fetchonce. Every response proves the network works and becomes a latency sample. A network error raises a suspicion.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.
| State field | Where you get it |
|---|---|
| online, since | Every browser |
| rtt, quality | Every browser, once one request or probe has finished |
| downlink | Chrome, Edge, Firefox, Safari 16.4 and later |
| effectiveType, saveData | Chrome 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
.mdto 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 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
watchFetchabout 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.