Web Bluetooth
Web Bluetooth reaches past the device to BLE hardware nearby. The API itself is 30 lines. The hard part is the GATT protocol -- finding service and characteristic UUIDs that are mostly manufacturer-defined with no central registry.
Most browser APIs talk to hardware that is already part of the device. Web Bluetooth reaches past the device entirely.
It lets JavaScript communicate directly with Bluetooth Low Energy peripherals -- smart lights, heart rate monitors, custom sensors, anything BLE. No native app. No driver. No intermediary.
The cost is complexity. You are not calling a clean API. You are speaking a protocol.
BLE Only
This is the first thing to get right: Web Bluetooth does not cover all Bluetooth.
There is classic Bluetooth -- the protocol your speaker and headphones use. It handles audio streaming and file transfer at ranges up to 100 meters.
There is Bluetooth Low Energy (BLE) -- a different protocol designed for small devices that run for months on a coin battery. Lower bandwidth, shorter range (around 10 meters), ideal for sensors and IoT hardware.
Web Bluetooth talks to BLE devices only.
Audio services and file transfer (FTP over Bluetooth) are explicitly blocked even if the device exposes them. The user can pair a Bluetooth speaker at the OS level and the browser will use it as a normal audio output -- but your JavaScript cannot reach into the speaker's BLE layer.
The GATT Model
This is the mental model that makes everything else make sense.
BLE devices communicate through a protocol called GATT -- Generic Attribute Profile. Every BLE device exposes a GATT server. That server contains one or more services. Each service groups a category of functionality. Inside each service are characteristics -- individual data values you can read, write, or subscribe to.
Every layer has a UUID. Each service has one. Each characteristic has its own.
To communicate with any BLE device, you need both UUIDs ahead of time. You cannot discover them cleanly at runtime without prior knowledge.
Some service UUIDs are standardized. 0x180F is the Battery Service. 0x180D is Heart Rate. 0x1812 is Human Interface Device. If a device exposes these, you know exactly what to expect.
The rest are manufacturer-defined. The UUID is chosen by whoever built the device.
There is no central registry. You find manufacturer UUIDs by:
- Reading the manufacturer's documentation (if they published it)
- Searching GitHub or npm for the device name plus "Bluetooth protocol" -- the community has reverse-engineered many popular devices
- Running a Bluetooth packet sniffer while the native app communicates with the device
This is why Web Bluetooth is genuinely hard. The API itself is short. The protocol you are speaking against is not.
The Permission Model
You cannot scan for BLE devices in the area. The browser refuses to return the list.
This is the right call. A list of nearby Bluetooth devices would reveal the user's environment, the hardware they own, and in some spaces, who else is present. The permission model prevents this.
Instead, you call requestDevice() and the browser shows the user a picker dialog. The user selects the device. Your code receives only what the user chose:
const device = await navigator.bluetooth.requestDevice({
filters: [{ namePrefix: 'Govee' }],
optionalServices: ['0000ec88-0000-1000-8000-00805f9b34fb']
})filters narrows the picker. Pass acceptAllDevices: true to show everything nearby -- but that list is long. Smart TVs, phones, laptops, watches, neighboring devices. Filter by name prefix or a required service ID to make the dialog usable.
optionalServices is not actually optional. You must declare every service UUID you plan to access at request time. Trying to use a service you did not declare will throw a security error. The browser uses this list to show the user exactly what the page is asking to access.
Connecting and Communicating
Once you have the device reference, the connection follows four async steps:
// Connect to the device's GATT server
const server = await device.gatt.connect()
// Get the service (must match optionalServices)
const service = await server.getPrimaryService('0000ec88-0000-1000-8000-00805f9b34fb')
// Get the characteristic within that service
const characteristic = await service.getCharacteristic('00010203-0405-0607-0809-0a0b0c0d2b11')Each step is await. Each one can throw if the device disconnects mid-flow or the UUID is wrong.
Reading data:
const value = await characteristic.readValue() // returns DataView
const byte0 = value.getUint8(0)
const byte1 = value.getUint8(1)Subscribing to updates (the device pushes to you):
characteristic.addEventListener('characteristicvaluechanged', (event) => {
const data = event.target.value // DataView
const level = data.getUint8(0) // interpret per device spec
})
await characteristic.startNotifications()Sending a command:
const command = new Uint8Array([0x33, 0x05, 0x02, 0xFF, 0x80, 0x00, 0x00, 0x00])
await characteristic.writeValue(command)That byte sequence is device-specific. What 0x33 means, what 0x05 tells the device to do -- those answers come from the manufacturer's protocol spec or from someone who sniffed the communication.
ExpandWeb Bluetooth GATT model: four-step connection flow, the device/service/characteristic hierarchy, standard UUIDs, and what gets blocked
Binary Data Is the Hard Part
All BLE communication is binary. Reads return a DataView. Writes take a Uint8Array. If the characteristic spec says "byte 0 is command type, bytes 1-3 are RGB, byte 4 is brightness, byte 5 is a checksum" -- you calculate that manually.
This is not a JavaScript problem. Every language working with BLE at this level does the same work. The bytes are the protocol.
The practical shortcut: search for the device name plus "BLE protocol" or "Web Bluetooth." A surprising number of popular devices -- fitness bands, smart lights, thermometers -- have had their protocols documented by the community. Someone has usually already done the hard work.
What Is Blocked
Two service categories are explicitly inaccessible:
- Audio -- Bluetooth headphones and speakers use classic Bluetooth protocols for audio streaming. You cannot stream audio through Web Bluetooth. The user can pair the device at the OS level and the browser will use it as a regular audio output, but not through this API.
- File transfer -- OBEX and FTP services over Bluetooth are blocked.
These restrictions exist by design. Unrestricted low-level audio access would create new privacy and security surface area.
Support: Chromium Only
Web Bluetooth is light-green tier. Chrome, Edge, and other Chromium-based browsers support it. Firefox has not implemented it. Safari has not implemented it and Apple has stated they do not plan to.
Apple's position is that BLE access from web pages is a security risk. The Chrome team disagrees. The argument has been public for years and remains unresolved.
Brave browser, despite being Chromium-based, has removed Web Bluetooth. So "Chromium-based" is necessary but not sufficient.
if ('bluetooth' in navigator) {
// Web Bluetooth is available
}Compared to WebHID, Web Bluetooth targets the BLE GATT layer rather than the USB/Bluetooth HID profile. WebHID is the right choice for controllers and input devices with a known HID descriptor. Web Bluetooth is the right choice for custom BLE sensors, smart lights, and hardware that communicates through GATT services.
Bluetooth hardware communicates through the air. The next API in this chapter synthesizes audio through the speaker -- Web Audio is what lets the browser generate sound from scratch rather than just play files.
The Essentials
- BLE only. Classic Bluetooth -- audio streaming, file transfer -- is explicitly blocked.
- The GATT model has three layers: Device -> Service (UUID) -> Characteristic (UUID). You need both UUIDs before you can communicate. Most manufacturer UUIDs come from community reverse-engineering, not official docs.
- You cannot scan for devices.
requestDevice()shows a browser picker -- your code receives only what the user selects. - Declare all service UUIDs in
optionalServicesat request time. Accessing an undeclared service after connecting throws a security error. - Data is binary:
DataViewfor reads,Uint8Arrayfor writes. The byte format is device-specific with no universal standard. - Chromium only (and not all Chromium). Safari and Firefox have no plans to implement.
Further Reading and Watching
- Connect Bluetooth Devices using Javascript (YouTube) -- walks through
requestDevice, the GATT connection flow, and reading/writing characteristics with a real BLE device - Interact with Bluetooth Devices on the Web -- Chrome for Developers -- official guide covering filters,
optionalServices, the four-step connection flow, and handling disconnection