Capabilities · API
Web Bluetooth API
Published Updated
navigator.bluetooth.requestDevice() shows a browser-owned chooser listing nearby Bluetooth Low Energy devices that match the filters you pass, and resolves with the one BluetoothDevice the user picks. From that object a page connects to the device’s GATT server and reads, writes, or subscribes to characteristics, so a PWA can talk to a heart-rate strap, a printer, or a lamp without a native companion app.
The API ships in Chromium only. Chrome 56 supports it on Android, ChromeOS, and macOS, Chrome 70 added Windows, and Linux still needs chrome://flags/#enable-experimental-web-platform-features (implementation status, github.com); Edge 79, Samsung Internet 6.0, and Opera 43 follow Chrome. Firefox has an open tracking bug (674737), WebKit records a formal “oppose” position (standards-positions #570), and Android WebView does not expose navigator.bluetooth.
Syntax
Section titled “Syntax”navigator.bluetooth.requestDevice(options)navigator.bluetooth.getDevices()navigator.bluetooth.getAvailability()
device.gatt.connect()server.getPrimaryService(service)service.getCharacteristic(characteristic)characteristic.readValue()characteristic.writeValueWithResponse(value)characteristic.startNotifications()requestDevice() returns a Promise<BluetoothDevice>; getDevices() returns a Promise<sequence<BluetoothDevice>> of devices this origin was previously granted, without a prompt, and is behind chrome://flags/#enable-web-bluetooth-new-permissions-backend from Chrome 85 (BCD api.Bluetooth.getDevices). getAvailability() resolves to true when an adapter is present. The Bluetooth interface is [Exposed=Window, SecureContext]: it is not available in workers and navigator.bluetooth is undefined on insecure origins.
Parameters
Section titled “Parameters”requestDevice() takes one RequestDeviceOptions dictionary. Exactly one of filters or acceptAllDevices: true must be given; the chooser only lists devices that match at least one filter and no exclusion filter.
| Member | Type | Required | Description |
|---|---|---|---|
filters |
sequence<BluetoothLEScanFilterInit> |
One of filters / acceptAllDevices |
Devices matching any filter are shown. Must be non-empty when present. |
exclusionFilters |
sequence<BluetoothLEScanFilterInit> |
No | Devices matching any of these are hidden even if they match filters; requires filters. Chrome 114. |
optionalServices |
sequence<BluetoothServiceUUID> |
No, default [] |
Services the page may access after connecting that are not already named in a services filter. A service missing from both is unreachable with SecurityError. |
optionalManufacturerData |
sequence<unsigned short> |
No, default [] |
Company identifiers whose manufacturer data the page may read from advertisements. |
acceptAllDevices |
boolean |
One of filters / acceptAllDevices |
List every nearby device instead of filtering. |
Each BluetoothLEScanFilterInit needs at least one member.
| Member | Type | Description |
|---|---|---|
services |
sequence<BluetoothServiceUUID> |
Advertised services; a 16-bit alias (0x180f), a GATT name ('battery_service'), or a 128-bit UUID string. Must be non-empty. |
name |
DOMString |
Exact device name, at most 248 UTF-8 bytes. |
namePrefix |
DOMString |
Name prefix, non-empty and at most 248 UTF-8 bytes. |
manufacturerData |
sequence<BluetoothManufacturerDataFilterInit> |
{ companyIdentifier, dataPrefix?, mask? }; identifiers must be unique within one filter. Chrome 92. |
serviceData |
sequence<BluetoothServiceDataFilterInit> |
{ service, dataPrefix?, mask? }. |
getPrimaryService(), getCharacteristic(), and their plural forms take a BluetoothServiceUUID or BluetoothCharacteristicUUID in the same three spellings.
Exceptions
Section titled “Exceptions”requestDevice() rejects with one of the following; the TypeError checks run first, before any radio activity.
| Exception | Condition |
|---|---|
TypeError |
exclusionFilters without filters; both or neither of filters and acceptAllDevices: true; an empty filters or exclusionFilters array; a filter with no members; an empty services, manufacturerData, or serviceData array; a name or namePrefix over 248 UTF-8 bytes or an empty namePrefix; duplicate companyIdentifier values; an empty dataPrefix; a mask whose length differs from dataPrefix. |
SecurityError |
The document is not allowed to use the bluetooth Permissions Policy feature; the call has no transient user activation; or a filter or optionalServices entry names a blocklisted service UUID. |
NotFoundError |
The chooser closed with no device selected, or no adapter or matching device exists. |
The GATT methods reject differently. device.gatt.connect() rejects with NetworkError when the device is out of range or the link fails. getPrimaryService() and getCharacteristic() reject with SecurityError for a service not granted through filters or optionalServices or a blocklisted UUID, with NetworkError when gatt.connected is false, and with NotFoundError when the device lacks the requested item. readValue() and writeValueWithResponse() reject with SecurityError for UUIDs blocklisted for reads or writes, InvalidModificationError for a value over 512 bytes, InvalidStateError when the service or characteristic no longer exists, NotSupportedError when the device refuses the operation, and NetworkError on disconnect mid-operation. All of these are listed in the specification’s per-method steps linked from See also.
Examples
Section titled “Examples”Every example feature-detects navigator.bluetooth and shows what the page does without it; the first two must run from a click or similar handler because of the activation rule.
Reading a battery level with a fallback message
Section titled “Reading a battery level with a fallback message”Filter on the standard Battery Service so the chooser shows only devices that advertise it, connect, and read the single-byte Battery Level characteristic. When the API is missing the function returns null and the UI explains that the browser cannot reach Bluetooth devices.
async function readBatteryLevel() { if (!('bluetooth' in navigator)) return null;
const device = await navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }], }); const server = await device.gatt.connect(); const service = await server.getPrimaryService('battery_service'); const characteristic = await service.getCharacteristic('battery_level'); const value = await characteristic.readValue(); return value.getUint8(0);}
document.querySelector('#battery').addEventListener('click', async () => { const output = document.querySelector('#battery-output'); try { const level = await readBatteryLevel(); output.textContent = level === null ? 'This browser cannot access Bluetooth devices; check the device itself.' : `Battery: ${level}%`; } catch (err) { if (err.name === 'NotFoundError') return; // chooser closed output.textContent = `${err.name}: ${err.message}`; }});'battery_service' and 'battery_level' are GATT assigned names the browser maps to 0x180F and 0x2A19; a 128-bit UUID string works the same way for vendor services.
Subscribing to heart-rate notifications and surviving a disconnect
Section titled “Subscribing to heart-rate notifications and surviving a disconnect”Notifications arrive as characteristicvaluechanged events after startNotifications(). The device may walk out of range at any time, so listen for gattserverdisconnected on the device and offer a reconnect that calls gatt.connect() again on the same object, which needs no new chooser.
async function watchHeartRate(onBeat, onLost) { if (!('bluetooth' in navigator)) { onLost('Bluetooth is not available in this browser'); return; }
const device = await navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }], }); device.addEventListener('gattserverdisconnected', () => onLost('Device disconnected'));
const server = await device.gatt.connect(); const service = await server.getPrimaryService('heart_rate'); const measurement = await service.getCharacteristic('heart_rate_measurement');
measurement.addEventListener('characteristicvaluechanged', (event) => { const data = event.target.value; const is16Bit = data.getUint8(0) & 0x1; onBeat(is16Bit ? data.getUint16(1, true) : data.getUint8(1)); }); await measurement.startNotifications();}The first byte of a Heart Rate Measurement is a flags field; bit 0 says whether the rate that follows is 8 or 16 bits wide, which is why the handler branches before reading.
Checking for an adapter before showing a pairing button
Section titled “Checking for an adapter before showing a pairing button”getAvailability() tells a page whether the machine has a Bluetooth radio at all, so it can hide a “Connect” button on a desktop without one instead of letting the user hit NotFoundError. It does not prompt and needs no activation.
async function bluetoothUiState() { if (!('bluetooth' in navigator)) return 'unsupported'; const available = await navigator.bluetooth.getAvailability(); return available ? 'ready' : 'no-adapter';}
const state = await bluetoothUiState();document.querySelector('#connect').hidden = state !== 'ready';document.querySelector('#bluetooth-note').textContent = { unsupported: 'Use Chrome or Edge on a desktop or Android device to connect over Bluetooth.', 'no-adapter': 'No Bluetooth adapter was found on this device.', ready: '',}[state];Availability can change while the page is open (a USB dongle plugged in, Bluetooth toggled off); the availabilitychanged event on navigator.bluetooth reports the new value.
See also
Section titled “See also”- Web Serial API, the same chooser model for serial ports
- WebUSB API
- WebHID API
- Web Bluetooth: requestDevice() method (bluetooth.spec.whatwg.org)
- Web Bluetooth: getDevices() method (bluetooth.spec.whatwg.org)
- WebKit standards position: Web Bluetooth (github.com)
- Mozilla bug 674737: Implement Web Bluetooth (bugzilla.mozilla.org)
- Communicating with Bluetooth devices over JavaScript (developer.chrome.com)
Specifications
| Specification | Status |
|---|---|
| Web Bluetooth API | Community draft |
| Web Bluetooth: requestDevice() method | WHATWG living standard |
| Web Bluetooth: RequestDeviceOptions dictionary | WHATWG living standard |
- Legend
- Yes
- Partial
- Flag
- No
- Unknown
| Browser / Platform | Support | Versions | Confidence | Source | Notes |
|---|---|---|---|---|---|
| Chrome (Desktop) | Yes | 56 | high | source | — |
| Chrome (Android) | Yes | 56 | high | source | 1 |
| Edge (Desktop) | Yes | 79 | high | source | 2 |
| Firefox (Desktop) | No | — | high | source | 3 |
| Firefox (Android) | No | — | high | source | 45 |
| Safari (macOS) | No | — | high | source | 6 |
| Safari (iOS) | No | — | high | source | 78 |
| Samsung Internet | Yes | 6.0 | high | source | 9 |
| WebView (Android) | No | — | high | source | 10 |
| Opera | Yes | 43 | high | source | 11 |
- Derived by browser-compat-data mirroring from Chrome.
- Derived by browser-compat-data mirroring from Chrome.
- Implementation tracking: https://bugzil.la/674737.
- Implementation tracking: https://bugzil.la/674737.
- Derived by browser-compat-data mirroring from Firefox.
- Implementation tracking: https://webkit.org/b/101034.
- Implementation tracking: https://webkit.org/b/101034.
- Derived by browser-compat-data mirroring from Safari.
- Derived by browser-compat-data mirroring from Chrome Android.
- No WebView Android support is recorded in browser-compat-data.
- Mirrors Chrome's implementation (BCD `opera: mirror`): Linux support exists but is not enabled by default.