Skip to content

Web Bluetooth API: connecting to Bluetooth Low Energy devices

In one line: The Web Bluetooth API lets a web app scan for and connect to nearby Bluetooth Low Energy (BLE) devices — heart-rate monitors, smart locks, sensors, game controllers — through a browser-mediated device picker, without a native app. It is a WICG specification currently shipping in Chromium-based browsers.

Web Bluetooth is defined by a WICG (Web Incubator Community Group) specification, not a finalized W3C standard. It is available in Chrome and Edge (desktop and Android) but not in Firefox or Safari. Always feature-detect and provide a fallback or clear messaging for unsupported browsers.

async function connectHeartRateMonitor() {
// 1. Request device — shows the browser's device-chooser UI
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['heart_rate'] }],
// optionalServices: ['battery_service'],
});
// 2. Connect to the GATT server on the device
const server = await device.gatt.connect();
// 3. Get the primary service
const service = await server.getPrimaryService('heart_rate');
// 4. Get the characteristic
const characteristic = await service.getCharacteristic('heart_rate_measurement');
// 5. Subscribe to notifications
characteristic.addEventListener('characteristicvaluechanged', (event) => {
const value = event.target.value; // DataView
console.log('Heart rate:', value.getUint8(1), 'bpm');
});
await characteristic.startNotifications();
}

navigator.bluetooth.requestDevice() must be called from a user activation (a click, tap, or similar transient event). Calling it programmatically without a gesture throws a SecurityError. The picker cannot be pre-answered or bypassed.

The Web Bluetooth API is only available in secure contexts — HTTPS or localhost. On plain HTTP, navigator.bluetooth is undefined.

Web Bluetooth does not use the standard Permissions API permission names. Access is device-scoped: the user picks a specific device from the browser’s chooser, and only that device is accessible to the origin. There is no “grant Bluetooth access broadly” prompt — each requestDevice() call targets specific services via filters or acceptAllDevices.

  • Granted device access persists across page loads for the same origin (the user does not have to re-pick on every visit), but can be revoked in browser settings.
  • device.gatt.connect() can reconnect to a previously chosen device without showing the picker again — call it from a user gesture if the device is not already connected.
  • filters: an array of filter objects. Each can include services (GATT service UUIDs), name, namePrefix, or manufacturerData. Only devices matching a filter appear in the picker.
  • acceptAllDevices: true: shows all nearby BLE devices. Use only for development or when the device type cannot be known in advance; it requires listing every service you intend to use in optionalServices.

BLE communication is structured as a hierarchy:

  • Services group related functionality (e.g., heart_rate, battery_service).
  • Characteristics are individual data points within a service (e.g., heart_rate_measurement).
  • Descriptors provide metadata about a characteristic.

You can use the standard 16-bit Bluetooth SIG UUIDs (short names like 'heart_rate' resolve to their full UUID automatically) or full 128-bit custom UUIDs for vendor proprietary services.

See /compatibility/ for current per-browser data.

Decision question Recommended action Rationale
Targeting Chrome/Edge users? Web Bluetooth is available on Chrome desktop, Chrome Android, and Edge. Chromium-only at this time; Firefox and Safari do not implement it.
Need to work on Firefox/Safari too? Show a clear “not supported in this browser” message; consider a native companion app. No polyfill can replicate BLE access; set expectations early.
Connecting to a known device type (e.g., HR monitor)? Use specific filters: [{ services: ['heart_rate'] }]. Narrows the picker to compatible devices; avoids showing unrelated BLE devices.
Need to reconnect without prompting the picker again? Store device and call device.gatt.connect() in a click handler. The origin retains access to previously chosen devices.
  • Feature-detect: if (!navigator.bluetooth) before calling any Web Bluetooth method.
  • Serve the page over HTTPS — the API is unavailable on plain HTTP.
  • Call requestDevice() only from a user-gesture handler.
  • Use specific filters rather than acceptAllDevices in production.
  • List all services you need in filters or optionalServices — the browser blocks access to unlisted services.
  • Handle disconnection events (device.addEventListener('gattserverdisconnected', ...)) and implement reconnection logic.
  • Communicate clearly that this feature requires a Chromium-based browser.