· deepdives · 7 min read
Connecting the Unconnected: A Beginner's Guide to the Web Bluetooth API
A practical, beginner-friendly guide to the Web Bluetooth API: what it is, how it works, browser/security constraints, and step-by-step code examples to scan, connect, read, write and receive notifications from Bluetooth Low Energy devices in the browser.

Why Web Bluetooth?
Bluetooth Low Energy (BLE) powers countless small devices: heart-rate monitors, temperature sensors, smart lights, and many IoT peripherals. The Web Bluetooth API opens a path for web apps to communicate directly with BLE devices using standard web technologies (JavaScript, HTML). That means you can build browser-based dashboards, prototypes, and web apps that interact with nearby hardware - without native apps.
This guide walks through the essentials, explains how BLE concepts map to the Web Bluetooth API, and builds a small, real-world example to get you started.
What is the Web Bluetooth API?
The Web Bluetooth API is a browser API that lets web pages connect to Bluetooth Low Energy (BLE) devices using the Generic Attribute Profile (GATT). It focuses on BLE (not classic Bluetooth) and exposes device discovery, service/characteristic access, reading/writing values, and receiving characteristic notifications.
Authoritative references:
- Web Bluetooth on MDN: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API
- Web Bluetooth guide on web.dev: https://web.dev/bluetooth/
- BLE GATT specifications: https://www.bluetooth.com/specifications/gatt/
Quick BLE concepts (GATT, services, characteristics)
- GATT (Generic Attribute Profile): how data is structured and accessed on BLE devices.
- Service: a grouping of related data (e.g., Battery Service).
- Characteristic: a single data item within a service (e.g., Battery Level). Characteristics can be readable, writable, and can send notifications.
- UUIDs: services and characteristics are identified by UUIDs; Bluetooth SIG publishes many standardized service/characteristic UUIDs (e.g., 0x180F for Battery Service).
Common built-in names (you can use either the 16-bit hex or the string key supported by the API):
- battery_service (0x180F)
- heart_rate (0x180D)
- battery_level (0x2A19)
- heart_rate_measurement (0x2A37)
Browser support and security constraints
Important runtime and security facts:
- Web Bluetooth requires a secure context (HTTPS) except on localhost for development.
- A user gesture is required to call
navigator.bluetooth.requestDevice()
; browsers show a device chooser UI and the user must explicitly select a device. - Browsers implement their own selection UIs - web pages cannot scan silently or enumerate devices without explicit user consent.
- Support: Chrome and Chromium-based browsers (desktop and Android) have broad support. Safari on iOS historically does not support Web Bluetooth. Check current browser compatibility before shipping.
- Permissions are ephemeral: by default, permission is granted per-origin per-device and can be revoked by the user or by clearing browser site data.
Refer to MDN / web.dev links above for up‑to‑date compatibility.
Getting started: prerequisites
- Serve your page over HTTPS (or use localhost for development).
- Use a supported browser (Chrome/Edge on desktop or Chrome on Android is the most reliable path).
- Have a BLE device in range.
- A simple UI button that the user clicks to open the browser’s device picker (user gesture requirement).
First app: Scan and connect (minimal example)
Below is a minimal, practical example that requests a device exposing a given service, connects, and reads a characteristic. It uses async/await for clarity.
HTML (minimal UI):
<button id="connect">Connect to Device</button>
<div id="status">Idle</div>
<pre id="output"></pre>
JavaScript:
const connectButton = document.getElementById('connect');
const statusEl = document.getElementById('status');
const output = document.getElementById('output');
connectButton.addEventListener('click', async () => {
try {
statusEl.textContent = 'Requesting device...';
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['battery_service'] }],
// Or use acceptAllDevices: true (but then you should request the right optionalServices later)
// acceptAllDevices: true,
optionalServices: ['battery_service'],
});
statusEl.textContent = `Connecting to ${device.name || device.id}...`;
const server = await device.gatt.connect();
statusEl.textContent = 'Getting Battery Service...';
const service = await server.getPrimaryService('battery_service');
const char = await service.getCharacteristic('battery_level');
const value = await char.readValue();
// battery_level is a single uint8
const batteryLevel = value.getUint8(0);
output.textContent = `Battery level: ${batteryLevel}%`;
statusEl.textContent = 'Connected';
// Remember to handle disconnects
device.addEventListener('gattserverdisconnected', onDisconnected);
} catch (error) {
statusEl.textContent = 'Error';
output.textContent = error;
console.error(error);
}
});
function onDisconnected(event) {
const device = event.target;
output.textContent = `${device.name || device.id} disconnected`;
}
Notes on the code:
requestDevice()
shows the browser’s device chooser. Thefilters
option limits the chooser to devices exposing the requested service(s).acceptAllDevices: true
will show all advertising devices but is less restrictive and not recommended for production.optionalServices
must include any service you want to access later that is not part of the initial advertised primary service.- Characteristic values are read as DataView objects; you parse them with methods like
getUint8
,getUint16
, etc.
Reading vs Writing vs Notifications
- readValue:
characteristic.readValue()
reads the current value once. - writeValue / writeValueWithoutResponse: write data to writable characteristics.
- startNotifications: subscribe to characteristic updates pushed by the device. You listen to
characteristicvaluechanged
events to receive updates.
Example: enabling notifications and handling incoming data
const characteristic = await service.getCharacteristic(
'heart_rate_measurement'
);
await characteristic.startNotifications();
characteristic.addEventListener('characteristicvaluechanged', event => {
const value = event.target.value; // DataView
// parse heart rate (example below)
const heartRate = parseHeartRate(value);
console.log('Heart rate:', heartRate);
});
Example parser for the Heart Rate Measurement characteristic (0x2A37)
function parseHeartRate(value) {
// value is a DataView
const flags = value.getUint8(0);
const hr16 = flags & 0x1; // 0 => uint8, 1 => uint16
let index = 1;
let heartRate;
if (hr16) {
heartRate = value.getUint16(index, /* littleEndian= */ true);
index += 2;
} else {
heartRate = value.getUint8(index);
index += 1;
}
return heartRate;
}
Refer to the Bluetooth SIG GATT spec for characteristic payload formats: https://www.bluetooth.com/specifications/gatt/
Example: Write a value (e.g., toggle a light)
If a characteristic supports writing, you can send bytes to it. Example for writing a single byte:
// assume `char` is a writable characteristic
const value = new Uint8Array([0x01]); // example payload
await char.writeValue(value);
Be mindful of endianness and expected value shapes for each characteristic.
Handling disconnects and reconnection strategies
BLE devices can disconnect unexpectedly. Good UX handles this gracefully:
- Listen for
gattserverdisconnected
and notify the user. - Offer a “Reconnect” button that calls
device.gatt.connect()
again. - Optionally implement an exponential backoff for automatic reconnect attempts, with a user-controlled toggle.
Example:
async function reconnect(device) {
try {
statusEl.textContent = 'Reconnecting...';
await device.gatt.connect();
statusEl.textContent = 'Reconnected';
} catch (err) {
console.error('Reconnect failed', err);
statusEl.textContent = 'Reconnect failed';
}
}
Note: some devices require the user to re-pair using the browser’s device picker if the OS has forgotten pairing or if the device requires a protected characteristic.
Filters vs acceptAllDevices and privacy
- filters: provide an array of filters for the device chooser (e.g., services, name, namePrefix). Filters reduce the options shown to the user and are preferred.
- acceptAllDevices: true allows any device to be chosen, but you must also include
optionalServices
for services you intend to access. Some browsers may require a stronger justification for using acceptAllDevices.
Example filter:
navigator.bluetooth.requestDevice({
filters: [{ namePrefix: 'MyDevice' }, { services: ['heart_rate'] }],
optionalServices: ['battery_service'],
});
Troubleshooting & common gotchas
- “navigator.bluetooth is undefined”: browser doesn’t support Web Bluetooth (or not in a secure context). Check browser compatibility and ensure HTTPS.
- “No devices found”: verify the device is powered/advertising, within range, and that the service you filter on is actually advertised by the device. Try
acceptAllDevices: true
for testing. - Permission denied / chooser closed: user dismissed the browser chooser - handle the exception and prompt the user.
- Characteristic not found: ensure the correct service/characteristic UUIDs and that
optionalServices
included the service if necessary. - Android location prompt: some Android configurations may request location permission from the user for Bluetooth scanning.
- iOS support: Safari on iOS historically lacks Web Bluetooth support (status may change-check current compatibility).
Best practices
- Keep UI interactions explicit: always trigger
requestDevice()
from a user gesture and explain to users why the device chooser appears. - Minimize requested services and permissions: only ask for services you actually need.
- Use
startNotifications()
rather than polling when the device supports notifications. - Gracefully handle disconnects and provide clear feedback.
- Test on target browsers and devices early - behavior varies.
- Be mindful of battery and energy: frequent writes/reads or high-frequency notifications will drain device batteries.
Real-world example ideas
- A web-based BLE thermometer dashboard that logs temperature over time.
- A PWA that connects to a BLE fitness tracker to display live metrics.
- A browser-based configuration tool for a BLE-enabled gadget (e.g., updating device settings).
For production deployments, consider offline handling, persisting device identifiers if you plan to offer reconnect flows, and clear user education about disconnects and permissions.
Wrapping up
The Web Bluetooth API brings hardware connectivity into the web platform in a powerful and user-friendly way. Start simple: build a small page that reads a battery level or heart rate, then expand toward notifications, writes, and better UI. Remember the security and UX constraints: HTTPS, user gestures, and the browser device chooser are there to protect users.
Further reading and references:
- MDN Web Bluetooth API: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API
- Web.dev guide (Google): https://web.dev/bluetooth/
- Bluetooth GATT specs (Bluetooth SIG): https://www.bluetooth.com/specifications/gatt/