To detect the network status in a Capacitor app, install the Capacitor Network plugin, call getStatus() to read the current connection, and attach a networkStatusChange listener to get notified whenever it changes. The plugin tells you whether the device is connected, how it is connected (Wi-Fi, cellular, ethernet, or VPN), and things the browser's navigator.onLine can't: whether the connection has verified internet access, whether it is metered, and whether the user has enabled a data-saving mode.
In this guide, we build network detection up step by step: reading the current status, reacting to changes, telling "connected" apart from "actually online", handling metered and data-saving connections, checking airplane mode, and putting it all together in an offline banner.
Key Takeaways
- The Capacitor Network plugin (
@capawesome/capacitor-network) reads the network status on Android, iOS, and the web through a single TypeScript API, with no permissions or configuration required. -
getStatus()returns the connection state, the connection type (WIFI,CELLULAR,ETHERNET,VPN,SATELLITE,NONE, orUNKNOWN), and flags for constrained, expensive, and ultra-constrained connections. - On Android,
internetReachablereflects the system'sNET_CAPABILITY_VALIDATEDcheck, so captive portals and dead VPN tunnels don't count as "online". -
constraineddetects Data Saver on Android and Low Data Mode on iOS;expensivedetects metered Wi-Fi and cellular connections. - The device is only observed while at least one listener is attached, so listening for changes doesn't cost battery when you don't need it.
Getting the Current Network Status
Reading the network status takes a single call to getStatus(). To install the Capacitor Network plugin first, please refer to the Installation section in the plugin documentation. Once installed, no configuration is needed; on Android, the plugin already declares the required ACCESS_NETWORK_STATE permission in its own manifest.
import { Network } from '@capawesome/capacitor-network';
const logNetworkStatus = async () => {
const status = await Network.getStatus();
console.log('Connected:', status.connected);
console.log('Connection type:', status.connectionType);
};
The returned GetStatusResult contains more than the two properties above:
-
connected: whether the device is connected to any network. -
connectionType: how it is connected, as a typedConnectionTypeenum (WIFI,CELLULAR,ETHERNET,VPN,SATELLITE,NONE, orUNKNOWN). -
internetReachable: whether the connection has verified access to the internet (Android only,nullelsewhere). -
constrained: whether a data-saving mode restricts the connection. -
expensive: whether the connection is metered. -
ultraConstrained: whether bandwidth is severely limited, for example on a carrier-provided satellite network.
A word on the null values you'll see in some of these properties: the plugin returns null wherever a platform can't determine the answer, instead of guessing. That makes the API honest about platform limits, and it's why the examples below compare against false explicitly rather than relying on truthiness.
Listening for Network Changes
Polling getStatus() is the wrong tool for reacting to connectivity drops; instead, register a listener for the networkStatusChange event and let the plugin push updates to you:
import { Network } from '@capawesome/capacitor-network';
const watchNetwork = async () => {
await Network.addListener('networkStatusChange', status => {
console.log('Network changed:', status.connectionType);
});
};
The listener receives the same GetStatusResult shape as getStatus(), so switching from Wi-Fi to cellular, losing the connection entirely, or entering Low Data Mode all arrive through the same event. The plugin only observes the device while at least one listener is attached, so there is no background cost once you clean up.
When your feature no longer needs updates, remove the listeners with removeAllListeners():
import { Network } from '@capawesome/capacitor-network';
const stopWatching = async () => {
await Network.removeAllListeners();
};
Why "Connected" Doesn't Mean Online
A device can be connected to a network without reaching the internet. The classic case is a captive portal: hotel or airport Wi-Fi reports a healthy connection, but every request is redirected to a login page until the user signs in. A VPN whose tunnel has silently died behaves the same way. If your app starts a sync the moment connected turns true, both cases produce failed requests and confused users.
This is what the internetReachable property is for. On Android, it reflects the NET_CAPABILITY_VALIDATED capability, meaning the operating system has actually verified that the connection reaches the internet:
import { Network } from '@capawesome/capacitor-network';
const canSync = async () => {
const { connected, internetReachable } = await Network.getStatus();
return internetReachable ?? connected;
};
On iOS and the web, internetReachable is always null, because those platforms can't distinguish validated internet access from mere connectivity. The ?? connected fallback above handles that cleanly: use the verified answer where the platform provides one, and fall back to the connection state everywhere else.
Detecting Metered and Data-Saving Connections
Not every connection should be treated equally, even when it works perfectly. Users on metered hotspots or limited data plans don't want your app to pull hundreds of megabytes in the background, and both Android (Data Saver) and iOS (Low Data Mode) let them say so system-wide. The expensive and constrained properties expose exactly these signals, so a download queue can respect them:
import { Network } from '@capawesome/capacitor-network';
const shouldDownloadLargeFiles = async () => {
const { connected, expensive, constrained } = await Network.getStatus();
return connected && expensive === false && constrained === false;
};
The strict === false comparisons matter here. Both properties are null on platforms that can't determine them (for example, most browsers), and treating "unknown" the same as "cheap and unrestricted" would defeat the purpose of the check.
Checking Airplane Mode on Android
When the connection type is NONE, it helps to tell the user why. On Android, isAirplaneModeEnabled() answers one common cause directly:
import { Network } from '@capawesome/capacitor-network';
const explainOffline = async () => {
const { enabled } = await Network.isAirplaneModeEnabled();
return enabled
? 'Airplane mode is on. Disable it to reconnect.'
: 'You are offline. Check your connection.';
};
This method is Android-only, since iOS offers no public API for reading the airplane mode state and browsers don't expose it either.
Building an Offline Banner
The most common use of network detection is also the simplest: an offline banner that appears when the connection drops and disappears when it comes back. Combining the initial status read with the change listener covers both the app launch and every change afterwards:
import { Network } from '@capawesome/capacitor-network';
const toggleOfflineBanner = (offline: boolean) => {
document.getElementById('offline-banner')?.classList.toggle('hidden', !offline);
};
const setupOfflineBanner = async () => {
const status = await Network.getStatus();
toggleOfflineBanner(!status.connected);
await Network.addListener('networkStatusChange', status => {
toggleOfflineBanner(!(status.internetReachable ?? status.connected));
});
};
The same pattern maps directly to a state variable in Angular, React, or Vue: read once on startup, subscribe for changes, and drive the banner from a single boolean. Note the reachability fallback from earlier reappearing in the listener, so Android users behind a captive portal see the banner even though they are technically connected.
FAQ
What is the difference between connected and internetReachable?
connected tells you whether the device is on any network at all, while internetReachable tells you whether that network has verified access to the internet. The two disagree behind captive portals and broken VPN tunnels, where the device is connected but nothing gets through. internetReachable is only available on Android and is null on iOS and the web.
How is this plugin different from the official Capacitor Network plugin?
The official @capacitor/network plugin reports the connection state and a basic connection type. The Capacitor Network plugin from Capawesome additionally reports verified internet reachability on Android, data-saving and metered connection flags, satellite and ultra-constrained network detection, an airplane mode check, and distinguishes ethernet and VPN connections as their own connection types.
Do I need any permissions to detect the network status?
No. The plugin works without configuration on all platforms. The ACCESS_NETWORK_STATE permission it needs on Android is declared in the plugin's own manifest, so there is nothing to add to your app.
Does network detection work in the browser?
Yes. On the web, the plugin reads navigator.onLine and the Network Information API where the browser supports it. Properties that browsers can't provide, such as internetReachable, are null there.
Why is a VPN connection reported as UNKNOWN on iOS?
On iOS, the plugin reads the network status from the NWPathMonitor of the Network framework, which does not identify VPN tunnels as a distinct interface type. The VPN connection type is therefore only reported on platforms that can detect it, such as Android.
Conclusion
Detecting the network status in a Capacitor app comes down to two calls: getStatus() for the current state and a networkStatusChange listener for everything after. The properties beyond connected are where the real quality wins live: internetReachable keeps captive portals from looking like working connections, and expensive and constrained keep large downloads off networks where they hurt.
There is one connection type we deliberately skipped here: satellite. Detecting it, and adapting your app to its extreme bandwidth limits on Android 15+ and iOS 26, is covered in the Capacitor Network plugin documentation. If you have questions, join the Capawesome Discord server, and subscribe to the Capawesome newsletter to stay up to date with new plugins and guides.
Top comments (0)