Introduction to the WebHID API
The WebHID (Human Interface Device) API is a browser-based JavaScript interface that allows web applications to communicate directly with HID-class devices such as keyboards, mice, gamepads, joysticks, and specialized industrial or medical hardware. Before WebHID, developers had to rely on native applications, browser extensions, or platform-specific drivers to interact with these devices. With WebHID, the browser becomes a capable host for hardware interaction, opening the door to web-based configuration tools, gaming dashboards, and IoT control panels.
This guide walks through everything you need to know to start building with the WebHID API: what it is, why it matters, how to request and communicate with devices, and the best practices that keep your application secure and reliable.
What Is the WebHID API?
The WebHID API exposes the HID protocol to web pages. HID is a standard defined by the USB Implementers Forum that describes how devices like keyboards and game controllers send reports to hosts. Each HID device exposes one or more collections of reports, which are structured packets of data describing inputs (e.g., button presses), outputs (e.g., LED states), and feature reports (e.g., configuration settings).
WebHID provides a JavaScript interface to enumerate, connect to, and exchange these reports with HID devices. It works on top of the operating system's HID stack, so no custom drivers are required. The API is asynchronous and Promise-based, fitting naturally into modern web development workflows.
Browser Support and Security Model
WebHID is currently supported in Chromium-based browsers (Chrome, Edge, Opera) on desktop platforms. Firefox and Safari have not shipped full support as of this writing, so always feature-detect before relying on the API.
Because HID devices can be powerful input sources, the API is gated behind a secure context (HTTPS or localhost) and requires explicit user permission. A user gesture, such as a button click, must trigger the connection request. Devices are not silently accessible.
Why WebHID Matters
- Zero-install tooling: Configuration utilities for game controllers, MIDI devices, or industrial hardware can run entirely in the browser.
- Cross-platform reach: A single web app can target Windows, macOS, Linux, and ChromeOS without per-platform binaries.
- Rapid prototyping: Hardware teams can ship internal tools quickly without packaging native installers.
- Integration with web ecosystems: HID data can feed directly into Web Audio, Canvas, WebGL, or WebRTC pipelines.
- Lower friction for users: No driver downloads, no admin rights, no app store approval.
Getting Started with WebHID
Feature Detection
Always check for WebHID support before attempting to use it. The API is exposed on navigator.hid.
if (!("hid" in navigator)) {
console.error("WebHID is not supported in this browser.");
} else {
console.log("WebHID is available.");
}
Requesting a Device
To connect to a device, call navigator.hid.requestDevice() with a filter that matches the device's vendor ID (VID) and product ID (PID). These 16-bit hexadecimal values are found in the device's documentation or via tools like lsusb on Linux.
const requestButton = document.getElementById("connect-btn");
requestButton.addEventListener("click", async () => {
try {
const devices = await navigator.hid.requestDevice({
filters: [
{ vendorId: 0x1234, productId: 0xabcd }
]
});
if (devices.length === 0) {
console.log("No device selected.");
return;
}
const device = devices[0];
console.log("Selected device:", device.productName);
} catch (error) {
console.error("Request failed:", error);
}
});
If you omit the filters array, the browser will show all HID devices to the user. This is useful for debugging but should be avoided in production for a smoother user experience.
Opening and Closing a Connection
Once a device is selected, you must open it before exchanging data. Use device.open() and device.close().
async function connect(device) {
if (!device.opened) {
await device.open();
console.log("Device opened.");
}
}
async function disconnect(device) {
if (device.opened) {
await device.close();
console.log("Device closed.");
}
}
Reading and Writing Reports
Listening for Input Reports
HID devices send input reports to the host. Register an inputreport event listener on the device to receive them. The event includes a data property (a DataView) and a reportId for devices that use numbered reports.
device.addEventListener("inputreport", (event) => {
const { device, reportId, data } = event;
const bytes = new Uint8Array(data.buffer);
console.log(`Report ID: ${reportId}`);
console.log("Bytes:", bytes);
// Example: parse the first byte as a bitmask of buttons
const buttonStates = bytes[0];
console.log("Button bitmask:", buttonStates.toString(2));
});
Sending Output Reports
Output reports are sent from the host to the device, commonly used to control LEDs, rumble motors, or display panels. Use device.sendReport(), passing the report ID (or 0 if the device uses only a single report) and a BufferSource.
async function setLed(device, reportId, ledIndex, on) {
const buffer = new Uint8Array(2);
buffer[0] = ledIndex;
buffer[1] = on ? 1 : 0;
await device.sendReport(reportId, buffer);
console.log("Output report sent.");
}
Feature Reports
Feature reports carry configuration data that is neither a continuous input nor a direct output. Read them with device.receiveFeatureReport() and write them with device.sendFeatureReport().
async function readFeature(device, reportId) {
const dataView = await device.receiveFeatureReport(reportId);
const bytes = new Uint8Array(dataView.buffer);
console.log("Feature report:", bytes);
return bytes;
}
async function writeFeature(device, reportId, payload) {
await device.sendFeatureReport(reportId, payload);
console.log("Feature report written.");
}
Putting It Together: A Gamepad LED Controller
The following example connects to a hypothetical gamepad, listens for button presses, and toggles an LED based on the first button. It demonstrates the full lifecycle: request, open, listen, send, and close.
const connectBtn = document.getElementById("connect");
const statusEl = document.getElementById("status");
let activeDevice = null;
connectBtn.addEventListener("click", async () => {
if (!("hid" in navigator)) {
statusEl.textContent = "WebHID not supported.";
return;
}
try {
const [device] = await navigator.hid.requestDevice({
filters: [{ vendorId: 0x1234, productId: 0xabcd }]
});
if (!device) return;
activeDevice = device;
await device.open();
statusEl.textContent = `Connected to ${device.productName}`;
device.addEventListener("inputreport", async (event) => {
const bytes = new Uint8Array(event.data.buffer);
const buttonPressed = (bytes[0] & 0x01) === 0x01;
// Toggle LED on report ID 1, byte 0
const ledBuffer = new Uint8Array([buttonPressed ? 1 : 0]);
try {
await device.sendReport(1, ledBuffer);
} catch (err) {
console.error("Failed to send report:", err);
}
});
device.addEventListener("disconnect", () => {
statusEl.textContent = "Device disconnected.";
activeDevice = null;
});
} catch (err) {
statusEl.textContent = `Error: ${err.message}`;
}
});
Handling Device Lifecycle Events
Devices can be connected or disconnected at any time. The navigator.hid object emits connect and disconnect events. Use these to keep your UI in sync and to auto-reconnect previously granted devices.
navigator.hid.addEventListener("connect", (event) => {
console.log("Device connected:", event.device.productName);
});
navigator.hid.addEventListener("disconnect", (event) => {
console.log("Device disconnected:", event.device.productName);
});
Reconnecting Previously Granted Devices
Once a user has granted access to a device, you can retrieve it later without showing the permission prompt again using navigator.hid.getDevices().
async function reconnectKnownDevices() {
const devices = await navigator.hid.getDevices();
for (const device of devices) {
if (!device.opened) {
await device.open();
console.log("Reconnected to", device.productName);
}
}
}
Best Practices
- Always feature-detect: Guard every entry point with
"hid" in navigatorand provide a graceful fallback message. - Use precise filters: Filter by VID and PID so users see only relevant devices in the chooser.
- Handle errors explicitly: Wrap all API calls in try/catch blocks. Devices can be unplugged mid-operation.
- Close devices when done: Call
device.close()when leaving a page section or when the user explicitly disconnects. This frees OS resources. - Validate report sizes: HID report layouts vary. Always check
data.byteLengthbefore indexing into the buffer. - Throttle output reports: Some devices rate-limit incoming reports. Avoid flooding
sendReport()in tight loops. - Respect user gestures: Permission requests must originate from a user action like a click. Do not auto-prompt on page load.
- Provide clear UI feedback: Show connection status, device names, and error messages so users understand what is happening.
- Test across platforms: HID descriptors can differ between operating systems. Validate on Windows, macOS, and Linux.
- Document report formats: If you are building a tool for a specific device, include a reference to the report layout in your code comments.
Common Pitfalls
One frequent mistake is assuming a fixed report ID. Some devices use report ID 0 to indicate that reports are not numbered, while others use multiple IDs. Inspect device.collections to understand the report structure before parsing.
function describeDevice(device) {
for (const collection of device.collections) {
console.log(`Usage: ${collection.usage} (page ${collection.usagePage})`);
for (const input of collection.inputReports) {
console.log(` Input report ${input.reportId}, items: ${input.items.length}`);
}
for (const output of collection.outputReports) {
console.log(` Output report ${output.reportId}, items: ${output.items.length}`);
}
for (const feature of collection.featureReports) {
console.log(` Feature report ${feature.reportId}, items: ${feature.items.length}`);
}
}
}
Another pitfall is forgetting that DataView byte order is big-endian by default. Use data.getUint8(i) for single bytes or specify the endianness flag for multi-byte reads.
Conclusion
The WebHID API brings low-level hardware interaction into the browser, eliminating the need for native installers while preserving the security model that keeps users in control. By combining precise device filters, careful report parsing, robust error handling, and thoughtful lifecycle management, you can build web applications that read gamepad inputs, configure industrial sensors, drive custom LED displays, and much more. As browser support matures, WebHID will continue to expand what is possible on the web platform, making the browser a first-class environment for hardware-aware applications. Start small with a single device, validate your report parsing thoroughly, and you will quickly discover how capable the web has become as a host for HID hardware.