Understanding WebHID
The WebHID API provides a way for web applications to interact with Human Interface Devices (HIDs) such as keyboards, mice, gamepads, barcode scanners, and many other specialized input/output hardware. It extends the browser’s capabilities beyond standard keyboard, mouse, and touch events, giving developers direct, low-level access to device reports through a secure, user-mediated permission model.
HIDs follow a well-defined USB or Bluetooth HID profile and exchange data in the form of input and output reports. WebHID lets you receive those input reports and send output reports, enabling features like reading a barcode scanner’s raw data, controlling an LED on a gamepad, or integrating a custom medical instrument with a web dashboard.
Why WebHID Matters
Before WebHID, web developers had limited options for hardware interaction. The Gamepad API supports game controllers but only exposes standardized axis and button states. Generic USB or Bluetooth APIs exist but often require complex handshake logic. WebHID fills a crucial gap:
- Specialized peripherals – barcode scanners, foot pedals, presentation clickers, industrial sensors.
- Custom dashboards – read telemetry from a scientific instrument directly in a web UI.
- Assistive technology – create alternative input methods for users with disabilities.
- Firmware updates – some devices expose HID interfaces for configuration and updates.
By offering a standardized, permission-gated interface, WebHID enables these use cases without requiring native app installations or proprietary browser plugins.
How to Use WebHID
The API lives on navigator.hid in secure contexts (HTTPS). The typical workflow consists of:
- Feature detection and permission request.
- Device selection via
requestDevice. - Opening a connection and listening for input reports.
- Sending output reports when needed.
1. Feature Detection & Secure Context
Always check for API availability. WebHID requires a secure origin (HTTPS or localhost) and a user gesture (click, key press) to show the device chooser dialog.
<script>
if ("hid" in navigator) {
console.log("WebHID is available");
} else {
console.log("WebHID not supported – consider a polyfill or fallback");
}
</script>
2. Requesting a Device
Use navigator.hid.requestDevice() with filter options to narrow the device list. Filters can
include vendorId, productId, usagePage, and usage.
This call must be triggered by a user gesture.
<button id="connectBtn">Connect to Scanner</button>
<script>
document.getElementById('connectBtn').addEventListener('click', async () => {
try {
const devices = await navigator.hid.requestDevice({
filters: [
{ vendorId: 0x1234, productId: 0x5678 }, // specific device
{ usagePage: 0x0001, usage: 0x0006 } // generic keyboard HID usage
]
});
// User selected a device, or the array is empty if cancelled
if (devices.length === 0) {
console.log("No device selected");
return;
}
const device = devices[0];
// Continue with opening the device...
} catch (err) {
console.error("requestDevice failed:", err);
}
});
</script>
3. Opening the Device & Listening for Reports
Once you have a HIDDevice object, call device.open(). Then attach an
inputreport event listener to receive data. The event’s data property is a
DataView on the raw report buffer.
let openedDevice = null;
async function openDevice(device) {
try {
await device.open();
openedDevice = device;
console.log("Device opened:", device.productName);
device.addEventListener("inputreport", (event) => {
const view = event.data;
const reportId = event.reportId;
// Parse the buffer according to the device’s descriptor
const firstByte = view.getUint8(0);
console.log(`Report ID ${reportId}, first byte: 0x${firstByte.toString(16)}`);
// Process further...
});
} catch (err) {
console.error("Failed to open device:", err);
}
}
4. Sending Output Reports
To send commands back to the device, use device.sendReport(). You need to know the correct
report ID and payload structure from the device’s HID descriptor.
async function sendCommand(reportId, dataArray) {
if (!openedDevice) {
console.warn("No device opened");
return;
}
// Create a Uint8Array with the payload (report ID is often the first byte)
const payload = new Uint8Array(dataArray.length + 1);
payload[0] = reportId;
payload.set(dataArray, 1);
try {
await openedDevice.sendReport(reportId, payload.buffer);
console.log("Output report sent successfully");
} catch (err) {
console.error("sendReport failed:", err);
}
}
5. Handling Disconnection
Devices can be physically disconnected or reset. Listen for the disconnect event on the
device to clean up state.
device.addEventListener("disconnect", () => {
console.log("Device disconnected");
openedDevice = null;
// Update UI to show disconnected status
});
Best Practices
-
User gesture requirement – always trigger
requestDeviceinside a click, keydown, or similar user interaction handler. Otherwise the browser will reject the call. -
Narrow filters – use specific
vendorId/productIdpairs when targeting a known device. UseusagePageandusageto filter by functionality without tying to exact hardware IDs. - Secure contexts only – WebHID will not work on plain HTTP. Deploy your app over HTTPS or use localhost during development.
-
Permission persistence – once a user grants access for a particular device/origin pair,
the permission may persist. Use
navigator.hid.getDevices()to retrieve previously authorized devices without showing the chooser again.const grantedDevices = await navigator.hid.getDevices(); if (grantedDevices.length > 0) { // Reconnect to a previously approved device openDevice(grantedDevices[0]); } else { // Fall back to requestDevice flow } - Handle empty selections – when the user cancels the device picker, the returned array is empty, not an error. Check array length rather than catching exceptions.
-
Report parsing – always consult the HID descriptor (vendor‑provided documentation) to
interpret the raw report buffer. The
DataViewgives you flexibility (getUint8, getInt16, etc.) but you must know the correct byte offsets and sizes. -
Error recovery – wrap
open(),sendReport(), and other calls in try/catch. Devices may become busy, enter an invalid state, or reject certain reports. -
UI feedback – show connection status, device name, and disconnection alerts to keep
users informed. Provide a manual reconnect button that calls
requestDeviceagain.
Complete Example: Barcode Scanner Reader
This example connects to a HID barcode scanner (usagePage 0x000C, usage 0x0001 for
consumer/point‑of‑sale devices) and displays scanned codes. It demonstrates the full lifecycle.
<!DOCTYPE html>
<html>
<head>
<title>WebHID Barcode Scanner</title>
</head>
<body>
<button id="connect">Connect Scanner</button>
<p id="status">Not connected</p>
<pre id="output"></pre>
<script>
const connectBtn = document.getElementById('connect');
const statusEl = document.getElementById('status');
const outputEl = document.getElementById('output');
let activeDevice = null;
connectBtn.addEventListener('click', async () => {
try {
const devices = await navigator.hid.requestDevice({
filters: [{ usagePage: 0x000C, usage: 0x0001 }] // consumer / barcode scanner
});
if (devices.length === 0) {
statusEl.textContent = 'No device selected';
return;
}
const device = devices[0];
await device.open();
activeDevice = device;
statusEl.textContent = `Connected: ${device.productName || 'Unknown device'}`;
device.addEventListener('inputreport', (event) => {
const view = event.data;
// Simple parser: assume ASCII characters starting at byte 0
let text = '';
for (let i = 0; i < view.byteLength; i++) {
const charCode = view.getUint8(i);
if (charCode !== 0) text += String.fromCharCode(charCode);
}
if (text.trim()) {
outputEl.textContent += text + '\n';
}
});
device.addEventListener('disconnect', () => {
statusEl.textContent = 'Device disconnected';
activeDevice = null;
outputEl.textContent += '[DISCONNECTED]\n';
});
} catch (err) {
statusEl.textContent = `Error: ${err.message}`;
console.error(err);
}
});
</script>
</body>
</html>
Conclusion
The WebHID API unlocks a new category of web applications that can directly interact with a wide range of human interface devices. By following the permission‑based, user‑mediated model, developers gain powerful hardware access while maintaining security and user trust. Focus on precise device filters, robust error handling, and clear user feedback to create reliable and intuitive experiences. As the API evolves, expect even broader device support and deeper integration with progressive web apps, bringing native‑grade peripheral control to the web platform.