|
1 | | -# Obs |
| 1 | +# Obs.js |
2 | 2 |
|
3 | | -> Nursing observations (obs) are routine checks to monitor your body while you |
4 | | -> recover during your admission. |
5 | | -> — [NHS East Suffolk and North Essex](https://www.esneft.nhs.uk/leaflet/our-quick-guide-to-nursing-observations/) |
| 3 | +Obs.js uses the Navigator and Battery APIs to get contextual information about |
| 4 | +your users’ connection strength and battery status. |
| 5 | + |
| 6 | +You can use this data to adapt your site/app to their environment, or beacon the |
| 7 | +data off to an analytics endpoint. |
| 8 | + |
| 9 | +At its simplest, Obs.js will add a suite of classes to your `<html>` element, |
| 10 | +e.g.: |
| 11 | + |
| 12 | +```html |
| 13 | +<html class="has-latency-low |
| 14 | + has-bandwidth-high |
| 15 | + has-battery-charging |
| 16 | + has-connection-capability-strong |
| 17 | + has-conservation-preference-neutral |
| 18 | + has-delivery-mode-rich"> |
| 19 | +``` |
| 20 | + |
| 21 | +This means you could do something like this: |
| 22 | + |
| 23 | +```css |
| 24 | +/** |
| 25 | + * Disable all animations and transitions if a user’s battery is below 5%. |
| 26 | + */ |
| 27 | +.has-battery-critical, |
| 28 | +.has-battery-critical * { |
| 29 | + animation: none; |
| 30 | + transition: none; |
| 31 | +} |
| 32 | +``` |
| 33 | + |
| 34 | +Or this: |
| 35 | + |
| 36 | +```css |
| 37 | +body { |
| 38 | + background-image: url('hi-res.jpg'); |
| 39 | +} |
| 40 | + |
| 41 | +/** |
| 42 | + * Show low-resolution images if the user can’t take rich media right now. |
| 43 | + */ |
| 44 | +.has-delivery-mode-lite body { |
| 45 | + background-image: url('lo-res.jpg'); |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +It also exposes this, and more, information via the `window.obs` object: |
| 50 | + |
| 51 | +```js |
| 52 | +{ |
| 53 | + "config": { |
| 54 | + "observeChanges": false |
| 55 | + }, |
| 56 | + "dataSaver": false, |
| 57 | + "rttBucket": 50, |
| 58 | + "rttCategory": "low", |
| 59 | + "downlinkBucket": 10, |
| 60 | + "connectionCapability": "strong", |
| 61 | + "conservationPreference": "neutral", |
| 62 | + "deliveryMode": "rich", |
| 63 | + "canShowRichMedia": true, |
| 64 | + "shouldAvoidRichMedia": false, |
| 65 | + "batteryCritical": false, |
| 66 | + "batteryLow": false, |
| 67 | + "batteryCharging": true |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +This means you could do something like this: |
| 72 | + |
| 73 | +```html |
| 74 | +<!-- |
| 75 | + - Fetch low-resolution poster/placeholder image regardless. |
| 76 | + --> |
| 77 | +<link rel=preload as=image href=poster.jpg> |
| 78 | + |
| 79 | +<div class=media-placeholder style="background-image: url(poster.jpg);"> |
| 80 | + |
| 81 | + <script> |
| 82 | +
|
| 83 | + const mediaPlaceholder = document.querySelector('.media-placeholder'); |
| 84 | +
|
| 85 | + if (window.obs && window.obs.canShowRichMedia) { |
| 86 | + // If we can show rich media, load the video with the poster image in place. |
| 87 | + const v = document.createElement('video'); |
| 88 | + v.src = 'video.mp4'; |
| 89 | + v.poster = 'poster.jpg'; |
| 90 | + v.autoplay = true; |
| 91 | + v.muted = true; |
| 92 | + v.playsInline = true; |
| 93 | + v.setAttribute('controls', ''); |
| 94 | + mediaPlaceholder.replaceChildren(v); |
| 95 | + } else { |
| 96 | + // If not, just show the poster image as an image element. |
| 97 | + const img = new Image(); |
| 98 | + img.src = 'poster.jpg'; |
| 99 | + img.alt = ''; |
| 100 | + mediaPlaceholder.replaceChildren(img); |
| 101 | + } |
| 102 | +
|
| 103 | + </script> |
| 104 | + |
| 105 | +</div> |
| 106 | +``` |
| 107 | + |
| 108 | +## Installation |
| 109 | + |
| 110 | +Obs.js **MUST** be placed in an inline `<script>` tag in the `<head>` of your |
| 111 | +document, before any other scripts, stylesheets, or HTML that may depend on it. |
| 112 | + |
| 113 | +### Listen for Changes |
| 114 | + |
| 115 | +If you have long-lived pages or a single-page app, you can instruct Obs.js to |
| 116 | +listen for changes to the connection and battery status by setting the following |
| 117 | +config: |
| 118 | + |
| 119 | +```html |
| 120 | +<script>window.obs = { config: { observeChanges: true } }</script> |
| 121 | + |
| 122 | +<script> |
| 123 | + // Obs.js |
| 124 | +</script> |
| 125 | +``` |
| 126 | + |
| 127 | +The default is `false`, which means Obs.js will only run once on each page load. |
| 128 | +This is sufficient for most non-SPA sites. |
| 129 | + |
| 130 | +## Statuses and Stances |
| 131 | + |
| 132 | +The information provided by Obs.js is split into two categories: **Statuses** |
| 133 | +and **Stances**. |
| 134 | + |
| 135 | +* A **Status** is a factual piece of information, such as whether the user has |
| 136 | + enabled Data Saver, or whether their battery is charging, or if they are on |
| 137 | + a high latency connection. |
| 138 | +* A **Stance** is an opinion derived from Statuses. For example, if the user has |
| 139 | + enabled Data Saver or their battery is low, we might say they have |
| 140 | + a **conservation preference** of `conserve`, meaning they might prefer to save |
| 141 | + resources. |
| 142 | + |
| 143 | +You can use either Statuses or Stances in your CSS or JavaScript. |
| 144 | + |
| 145 | +## Available CSS Classes and JS Properties |
| 146 | + |
| 147 | +Obs.js exposes the following classes under the following conditions: |
| 148 | + |
| 149 | +| Class | Meaning | Computed/derived from | |
| 150 | +| --------------------------------------- | ------------------------ | --------------------------------------------------------------- | |
| 151 | +| `.has-data-saver` | User enabled Data Saver | `navigator.connection.saveData === true` | |
| 152 | +| `.has-battery-critical` | Battery ≤ 5% | `battery.level ≤ 0.05` | |
| 153 | +| `.has-battery-low` | 5% < battery ≤ 20% | `0.05 < battery.level ≤ 0.2` (mutually exclusive with critical) | |
| 154 | +| `.has-battery-charging` | On charge | `battery.charging === true` | |
| 155 | +| `.has-latency-low` | Low RTT | `rtt < 75ms` | |
| 156 | +| `.has-latency-medium` | Medium RTT | `75–275ms` | |
| 157 | +| `.has-latency-high` | High RTT | `> 275ms` | |
| 158 | +| `.has-bandwidth-low` | Low estimated bandwidth | `downlinkBucket ≤ 5` (1 Mbps buckets via `Math.ceil`) | |
| 159 | +| `.has-bandwidth-high` | High estimated bandwidth | `downlinkBucket ≥ 8` (6–7 is a dead zone → no class) | |
| 160 | +| `.has-connection-capability-strong` | Transport looks strong | `latency = low` **and** `bandwidth = high` | |
| 161 | +| `.has-connection-capability-moderate` | Transport middling | Anything not strong/weak | |
| 162 | +| `.has-connection-capability-weak` | Transport looks weak | `latency = high` **or** `bandwidth = low` | |
| 163 | +| `.has-conservation-preference-conserve` | Frugality signal present | `dataSaver === true` **or** `batteryLow === true` | |
| 164 | +| `.has-conservation-preference-neutral` | No frugality signal | Battery isn’t low and Data Saver is not enabled | |
| 165 | + |
| 166 | +These classes are automatically added to the `<html>` element. |
| 167 | + |
| 168 | +Obs.js also stores the following properties on the `window.obs` object: |
| 169 | + |
| 170 | +| Property | Type | Meaning | Computed/derived from | Notes | |
| 171 | +| ------------------------ | ---------------------------------- | --------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | |
| 172 | +| `config.observeChanges` | boolean | Whether Obs.js attaches change listeners | **Default `false`**; set by you _before_ Obs.js runs | Opt-in for SPAs or long-lived pages | |
| 173 | +| `dataSaver` | boolean | User enabled Data Saver | `navigator.connection.saveData` | — | |
| 174 | +| `rttBucket` | number (ms) | RTT bucketed to **ceil** 25 ms (e.g. 101→125) | `navigator.connection.rtt` | Undefined if Connection API missing | |
| 175 | +| `rttCategory` | `'low' \| 'medium' \| 'high'` | CrUX tri-bin: `<75`, `75–275`, `>275` | Derived from `rtt` | Drives latency classes | |
| 176 | +| `downlinkBucket` | number (Mbps) | Downlink bucketed to **ceil** 1 Mbps | `navigator.connection.downlink` | Low/High thresholds: `≤5` / `≥8` | |
| 177 | +| `downlinkMax` | number (Mbps) | Max estimated downlink (if exposed) | `navigator.connection.downlinkMax` | Not used for Stances; informational only | |
| 178 | +| `connectionCapability` | `'strong' \| 'moderate' \| 'weak'` | Transport assessment | Derived from `rttCategory` and `downlinkBucket` | Strong = low RTT **and** high BW; Weak = high RTT **or** low BW | |
| 179 | +| `conservationPreference` | `'conserve' \| 'neutral'` | Frugality signal | `dataSaver === true` **or** `batteryLow === true` → `conserve` | — | |
| 180 | +| `deliveryMode` | `'rich' \| 'cautious' \| 'lite'` | How “heavy” you should go | Derived from capability and conservation | Rich if **strong** and **not** conserving; Lite if **weak** or **conserve**; else Cautious | |
| 181 | +| `canShowRichMedia` | boolean | Convenience: `deliveryMode === 'rich'` | Derived from `deliveryMode` | Shorthand for “go big” | |
| 182 | +| `shouldAvoidRichMedia` | boolean | Convenience: `deliveryMode === 'lite'` | Derived from `deliveryMode` | Shorthand for “be frugal” | |
| 183 | +| `batteryCritical` | boolean \| null | Battery `≤ 5%` | Battery API | Mutually exclusive with `batteryLow`; `null` if unknown | |
| 184 | +| `batteryLow` | boolean \| null | `5% < level ≤ 20%` | Battery API | `true` only when not `batteryCritical` | |
| 185 | +| `batteryCharging` | boolean \| null | On charge | Battery API | `null` if unknown | |
0 commit comments