← Back to DevBytes

WebHID API: Complete Guide

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

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

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles