Obs.js turns connection and battery state into classes you can style against
Context-aware web performance for everyone
At a glance
- What is it?
- Obs.js reads the Navigator and Battery APIs and publishes what it finds as class names on the html element and as an object on window, so a page can decide whether to send a video or a poster image before it knows anything about the visitor.
- Who is it for?
- Obs.js suits a team that has a heavy media page and wants to send the right asset to a phone on a weak connection without a server round trip, and it suits anyone who wants to disable animations for a user on a low battery. It is the wrong tool if you need to know anything the browser does not expose, because everything Obs.js knows comes from the Navigator and Battery APIs and nothing else.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 83 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The whole idea is in two sentences
Obs.js is described as context-aware web performance for everyone, with the tagline of meeting your users where they are. It uses the Navigator and Battery APIs to get contextual information about your users' connection strength and battery status, and you can use that data to adapt your site or app to their environment, or beacon the data off to an analytics endpoint. That is the complete pitch, and the appeal is the timing. The Browser decides when to send the heavy asset, not the server, and the decision is made from facts about the device rather than from a guess about a device class. The project is by Harry Roberts of csswizardry, licensed MIT, and the repository carries the topics battery-api, navigator-api, sitespeed, webperf and webperformance, which tells you the audience is performance-minded front end developers rather than a general site owner.
The simplest interface is a set of classes on the html element
At its simplest, Obs.js adds a suite of classes to your html element, and the example shows the shape of the result.
<html class="has-latency-low
has-bandwidth-high
has-battery-charging
has-connection-capability-strong
has-conservation-preference-neutral
has-delivery-mode-rich">Six class families cover the dimensions the script measures, and each family has a small fixed set of values. Latency and bandwidth are the network side, with buckets drawn from low, medium and high. Connection capability is the combined judgement, with strong, moderate and weak. Conservation preference is the two-value answer, conserve or neutral. Delivery mode is the three-value summary, rich, cautious or lite. And the battery family carries the charging and critical states. Because the mechanism is CSS classes, the adaptation layer is ordinary stylesheet rules, and the first example in the readme turns off every animation and transition for a user whose battery is below five percent.
.has-battery-critical,
.has-battery-critical * {
animation: none;
transition: none;
}The second example swaps a high-resolution background for a low-resolution one when the user cannot take rich media right now, which is the pattern the whole library exists to support.
.has-delivery-mode-lite body {
background-image: url('lo-res.jpg');
}The object on window carries the numbers and the derived flags
The same information is available as a window.obs object, and the readme shows what it contains. The configuration is part of it, with an adaptive flag and an observeChanges flag, and the rest is the state: dataSaver, rttBucket, rttCategory, downlinkBucket, connectionCapability, conservationPreference, deliveryMode, canShowRichMedia, shouldAvoidRichMedia, batteryCritical, batteryLow and batteryCharging. The buckets are rounded rather than raw, with the latency bucket rounded up to the next twenty-five milliseconds and the downlink bucket to whole units, and the categories are derived from those numbers, with a low latency meaning under seventy-five milliseconds and a high downlink meaning eight or more. That rounding is a deliberate choice, because a CSS class is a bucket and a number in an analytics beacon is the bucket too, and rounding keeps the two consistent. The two flags canShowRichMedia and shouldAvoidRichMedia are the ones an application actually branches on, and they are derived from the delivery mode rather than measured directly.
Two installation modes, and one of them has a hard rule
There are two main ways to install Obs.js, and the difference is about when the script runs. The adaptive mode must run early and inline, and it adds classes to the html element for CSS and JavaScript adaptation later on. The analytics mode can be deferred or external, and it does not add classes to the html element, but it still populates window.obs for analytics purposes. If you are using Obs.js for adaptation, it must be placed in an inline script tag in the head of your document, before any other scripts, stylesheets or HTML that may depend on it. The reason is obvious once stated, which is that a class applied after the first paint has already missed the moment it was supposed to influence, so the script ships with a guard that checks the current script element and warns if the placement is wrong. The warning is explicit, saying the script is skipping because it must be an inline, classic script in head, and it names the reason, whether it was a src, a module type, or something else.
Branching in JavaScript instead of CSS
The second half of the readme shows the same idea driven from script rather than from stylesheets, and it is the pattern for a page that has to swap a whole element. The markup is a placeholder div with a poster image, and the script checks window.obs and its canShowRichMedia flag before deciding what to insert.
The branching is a plain if on window.obs and its canShowRichMedia property, and the two branches are different in kind rather than in degree. When rich media is allowed, the script creates a video element with the poster image set as its poster, sets autoplay, muted and playsInline, adds the controls attribute, and replaces the placeholder with it. When it is not, the script creates a plain image element with the same source and an empty alt attribute, and replaces the placeholder with that. The two branches deliberately do the same amount of layout work, so the page does not reflow when the decision is made. The media placeholder itself is a div with the poster as a background image, and the video and the poster are the same file, which means the poster is already in the browser cache by the time the decision is taken.
How it is built and shipped
The repository is small and the build is a script rather than a bundler setup. The top level holds obs.js as the source, obs-speedcurve.js, a demo/ directory with an index.html and assets, scripts/ with the build script, and test/ with the tests. The package.json is marked private and uses ES modules, and the scripts are the whole toolchain. The build runs node scripts/build.mjs, the test script runs the build and then node --test, a separate test:source script runs the tests against the source file directly with node --test test/obs.test.mjs, and the watch mode uses chokidar-cli to rebuild whenever obs.js changes, with dev aliasing watch. The only two dev dependencies are chokidar-cli and terser, and terser is the minifier that produces the single-file inline build the readme tells you to paste into your head. That is the whole distribution model. There is no package to install and no runtime dependency, because a script that has to run inline before anything else cannot be a module and cannot have a dependency tree, which is a constraint that shapes the code and the release process at the same time.
Where this is the wrong choice
Three limits are worth naming. The first is that the data is only as good as the two browser APIs, and neither is guaranteed to exist. The Navigator API in particular is not implemented in every browser, and the Battery API is a moving target, so a page that depends on Obs.js for its layout has to have a sensible default, which in practice means designing as though delivery mode were rich and then degrading. The second is that the guard against wrong placement is a console warning rather than an error, so a misconfigured deployment fails quietly and the classes never appear, which is a confusing symptom for something that looks like a CSS bug. The third is the scope of what can be adapted. Obs.js tells you about the connection and the battery, and it does not tell you about the device class, the user's motion preference, the time of day or the size of the viewport, so a page that wants to personalise properly still needs a media query and a prefers-reduced-motion query alongside it. What Obs.js adds is the part those mechanisms cannot express, which is the case where the same device is sometimes fine and sometimes not.
Editorial conclusion
Obs.js suits a team that has a heavy media page and wants to send the right asset to a phone on a weak connection without a server round trip, and it suits anyone who wants to disable animations for a user on a low battery. It is the wrong tool if you need to know anything the browser does not expose, because everything Obs.js knows comes from the Navigator and Battery APIs and nothing else. It is also the wrong choice if you cannot put a script at the top of your head, since the adaptive mode refuses to run anywhere else. Before adopting it, read the installation rule about inline placement, because a deferred script silently changes the mode you are in, and note that the repository publishes the source as a single file with a build script rather than as a package. The last push was on 2026-08-01 and the current release is 0.3.0 from 2026-05-13.
Frequently asked questions
How do I install Obs.js?
There are two modes. For adaptation, Obs.js must be placed in an inline script tag in the head of your document, before any other scripts, stylesheets or HTML that may depend on it, and the script warns and skips itself if it is not an inline classic script there. For analytics only, the script can be deferred or external, since it only needs to populate window.obs and does not add classes to the html element.
What information does Obs.js expose?
It adds classes to the html element covering latency, bandwidth, connection capability, conservation preference, delivery mode and battery state, with values such as has-latency-low, has-bandwidth-high, has-battery-charging and has-delivery-mode-rich. It also exposes a window.obs object with dataSaver, rttBucket, rttCategory, downlinkBucket, connectionCapability, conservationPreference, deliveryMode, canShowRichMedia, shouldAvoidRichMedia, batteryCritical, batteryLow and batteryCharging.
How do I build Obs.js from source?
The repository is a private ES module package with two dev dependencies, chokidar-cli and terser. npm run build runs node scripts/build.mjs to produce the minified single file, npm test runs the build and then node --test, npm run test:source runs the tests against the source directly, and npm run watch rebuilds whenever obs.js changes.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/csswizardry-obs-js)Was this useful?