← Back to DevBytes

Implementing WebHID API in Modern Web Applications

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:

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:

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

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles