JavaScript Battery Status API Table
| Piece | What it does | Field note |
|---|---|---|
navigator.getBattery() | The entry | Promise to a live BatteryManager - no permission prompt, no options; hold the reference, never re-poll |
level | The fraction | 0-1, but Chrome rounds to 5% steps (v103+) - read as a range, never as a measurement |
charging + events | The live state | charging bool + four change events - no unified change event; one subscriber re-reads all four |
chargingTime/dischargingTime | The estimates | Infinity when unknown, full, or AC - guard with isFinite or the comparison is always true |
the nerf | The privacy story | Firefox removed it (fingerprinting), Safari never shipped - coarse by committee decision, not by accident |
use cases | What it is for | Save-before-death checkpoints, dim under 20%, defer heavy sync unplugged - coarse triggers only |
the fallback | Honest support | Gate on 'getBattery' in navigator - iOS and post-2016 Firefox show nothing, not a fake 100% |
no permission | The odd one | Passive read from the pre-Permissions era - the silence that made it a fingerprint vector |
The Battery Status API is one promise deep: navigator.getBattery() resolves to a live BatteryManager with four properties (charging, level, chargingTime, dischargingTime) and four change events. No permission prompt, no Permissions API entry, no options object - the least ceremony of any hardware API in the platform, which is precisely the trait that made it a problem.
Bottom line: treat every number as a hint with a precision budget. Chrome has rounded level to the nearest 5% since version 103 and blurs the fast-discharge tail; Firefox removed the API entirely in 2016 over fingerprinting, and Safari never shipped it. The honest code gates on 'getBattery' in navigator, degrades to nothing (not to an assumed 100%), and uses the value for coarse decisions - save state before death, dim the UI under 20%, defer heavy sync while unplugged - never for anything a user could call a measurement.
The honest part: the API is a mobile nicety wearing a desktop jacket. On plugged-in desktops it reports charging: true, level near 1, Infinity estimates - correct and useless. The scenarios that justify it live on phones and tablets: a checkpoint save as dischargingTime drops under an hour, a reading mode that pauses background polling at low level, an uploader that waits for the charging event. If your web app runs mostly in desktop browsers, this API will spend its life reporting Infinity at you.
How to use
- Gate and grab: if ('getBattery' in navigator) navigator.getBattery().then(b => {...}) - the promise resolves once; the BatteryManager it hands you is live, so hold the reference and listen rather than re-polling.
- Subscribe to the four events: chargingchange, levelchange, chargingtimechange, dischargingtimechange - a single listener per event turns the manager into a reactive source for your UI. There is no 'change' event covering all four; attach each.
- Read level as a fraction with a floor: b.level runs 0-1 (0.87 = 87%), but modern Chrome rounds to 5% steps - display ranges ('85-90%') rather than exact percentages, and never persist level as if it were a measurement.
- Handle Infinity as a state, not a number: chargingTime and dischargingTime are Infinity when the estimate is unavailable (full battery, AC power, or the browser simply declines). Comparing them with < without an isFinite() guard produces the classic always-true bug.
- Pick use cases that survive imprecision: checkpoint autosave when level < 0.15 and not charging, media quality downgrade below 0.2, and 'plugged in' badges from the charging event. Anything finer than that - per-app power budgets, countdown-to-death clocks - is beyond what the platform is willing to reveal.
Frequently asked questions
Why is the Battery API considered a privacy problem?
Because level and discharge curve are a fingerprint: for a short window, a battery's exact discharge rate (read twice, seconds apart) was effectively unique per device - the EFF's fingerprinting research used it as the textbook case of a harmless-seeming value identifying a specific phone. The platform's response was degradation rather than removal in Chrome: level rounded to 5% steps (version 103), charging state kept coarse. Firefox went further and removed the API outright; Safari never shipped it. The lesson generalizes: any sensor without a permission gate is a fingerprint until proven otherwise, and 'no prompt' is a warning sign, not a convenience.
Why does getBattery not ask for permission?
It predates the Permissions API discipline and shipped as a passive read - the same design generation as ambient light sensors. By the time the platform standardized user consent for sensors, battery had already earned its fingerprinting reputation, so it got precision-blurred instead of permission-gated: the committee answer was 'make the data useless rather than make it asked-for.' New hardware APIs (geolocation, motion on iOS, Bluetooth) all prompt; battery is the fossil that shows why the prompt layer exists. Do not architect around richer battery data arriving later - it will not.
How do I detect charging state changes reliably?
Hold the BatteryManager and listen for chargingchange - the boolean flips true the moment the cable connects, and level/estimates follow with their own events. The reliable pattern is one small subscriber function that re-reads all four properties on ANY of the four events, rather than reasoning about which event carries which update - browsers coalesce and reorder these in practice. And handle the unsupported branch as a first-class state: 'getBattery' absent means Safari/iOS and post-2016 Firefox; show nothing rather than a fake 100%.
What are chargingTime and dischargingTime actually good for?
Coarse triggers and honest zeros. dischargingTime shrinking under an hour is a real save-your-work signal; chargingTime under 20 minutes is a plausible 'wait for full before the big upload' hint. Beyond that, treat them as vibes: browsers extrapolate from recent drain, Android's estimates wobble with screen brightness, and Infinity - the value when charging completes, on AC power, or when the browser declines to guess - must be guarded with isFinite() before any arithmetic. The number the API is best at is the one it rounds: level, read as a range, driving decisions in fifths, not percentages.