# WebHID API

> navigator.hid.requestDevice() pairs a Human Interface Device and HIDDevice exchanges input, output, and feature reports. Filters, every exception, examples.

`navigator.hid.requestDevice()` shows a browser-owned chooser of connected Human Interface Devices that match the filters you pass and resolves with the `HIDDevice` objects the user grants. Once opened, a device delivers input reports as `inputreport` events and accepts output and feature reports as byte buffers, which lets a PWA drive a macro pad, a game controller, a barcode scanner, or a lab instrument that speaks HID over USB or Bluetooth.

Support is desktop Chromium only: Chrome 89 and Edge 89 on Windows, macOS, Linux, and ChromeOS implement `HID`, `HIDDevice`, `requestDevice()`, and `getDevices()`; Chrome 100 added `HIDDevice.forget()`, and Chrome 131 exposes `navigator.hid` in dedicated workers (BCD `api.HID`, `api.HID.worker_support`). Chrome for Android, Android WebView, Firefox, and Safari report no support, and WebKit has filed a formal "oppose" position ([standards-positions #510](https://github.com/WebKit/standards-positions/issues/510)).

## Syntax

```js
navigator.hid.requestDevice(options)
navigator.hid.getDevices()

device.open()
device.close()
device.forget()
device.sendReport(reportId, data)
device.sendFeatureReport(reportId, data)
device.receiveFeatureReport(reportId)
```

`requestDevice()` and `getDevices()` return a `Promise<sequence<HIDDevice>>`; `requestDevice()` resolves with an empty sequence, not a rejection, when the user dismisses the chooser. `open()`, `close()`, `forget()`, `sendReport()`, and `sendFeatureReport()` return `Promise<undefined>`; `receiveFeatureReport()` returns a `Promise<DataView>` whose first byte may be the report ID. `navigator.hid` is `[SecureContext]` and `requestDevice()` additionally needs a `Window` global with transient user activation. Each `HIDDevice` exposes `opened`, `vendorId`, `productId`, `productName`, and `collections`, and fires `inputreport` (`HIDInputReportEvent` with `device`, `reportId`, `data`); `navigator.hid` fires `connect` and `disconnect` (`HIDConnectionEvent` with `device`) for previously granted devices.

## Parameters

`requestDevice()` takes one `HIDDeviceRequestOptions` dictionary.

| Member | Type | Required | Description |
|---|---|---|---|
| `filters` | `sequence<HIDDeviceFilter>` | Yes | Devices matching any filter are listed; `[]` lists every connected HID device the browser does not block. |
| `exclusionFilters` | `sequence<HIDDeviceFilter>` | No | Devices matching any of these are hidden even when they match `filters`. Must be non-empty when present. |

Every `HIDDeviceFilter` member is optional, but a filter is only valid when `productId` comes with `vendorId` and `usage` comes with `usagePage`.

| Member | Type | Description |
|---|---|---|
| `vendorId` | `unsigned long` | USB-IF vendor ID, for example `0x046d` for Logitech. |
| `productId` | `unsigned short` | Product ID; requires `vendorId`. |
| `usagePage` | `unsigned short` | HID usage page of a top-level collection, for example `0x0001` Generic Desktop or `0xff00` and above for vendor-defined pages. |
| `usage` | `unsigned short` | Usage ID within `usagePage`; requires `usagePage`. |

`sendReport()`, `sendFeatureReport()`, and `receiveFeatureReport()` take a `reportId` (`octet`, `0` when the interface does not use report IDs) and, for the two send methods, a `BufferSource` `data` holding the report payload without the ID byte.

## Exceptions

`requestDevice()` and `getDevices()` reject before any chooser appears when one of the following holds.

| Exception | Condition |
|---|---|
| `SecurityError` | The document is not allowed to use the `hid` Permissions Policy feature, or (`requestDevice()` only) the call has no transient user activation. |
| `NotSupportedError` | `requestDevice()` was called from a global that is not a `Window`, such as a worker. |
| `TypeError` | A filter in `filters` or `exclusionFilters` is not valid (`productId` without `vendorId`, `usage` without `usagePage`), or `exclusionFilters` is present but empty. |

The `HIDDevice` methods reject as follows.

| Method | Exception | Condition |
|---|---|---|
| `open()` | `InvalidStateError` | The device state is not `"closed"` (already open, opening, or forgotten). |
| `open()` | `NetworkError` | The operating system refused to open the device. |
| `close()` | none | Resolves; any pending `sendReport()` or feature-report promise is rejected with `AbortError`. |
| `forget()` | `InvalidStateError` | The device is already `"forgotten"` or `"forgetting"`. |
| `sendReport()`, `sendFeatureReport()`, `receiveFeatureReport()` | `InvalidStateError` | The device is not `"opened"`. |
| the same three | `TypeError` | `reportId` is `0` on an interface that uses report IDs, or non-zero on one that does not. |
| the same three | `NotAllowedError` | The report is on the browser's blocked list (for example reports on protected keyboard or FIDO collections). |
| the same three | `NetworkError` | The operating system failed to write or read the report. |

A blocked input report raises no exception at all: the browser drops it without firing `inputreport`.

:::observed
Chrome rejects `navigator.hid.requestDevice({ filters: [] })` called outside a user-activation handler with `SecurityError: Must be handling a user gesture to show a permission request.`, `device.sendReport()` on a device that was never opened with `InvalidStateError: The device must be opened first.`, a second `device.open()` with `InvalidStateError: The device is already open.`, and `requestDevice({ filters: [], exclusionFilters: [] })` with `TypeError: Failed to execute 'requestDevice' on 'HID': 'exclusionFilters', if present, must contain at least one filter.`. The strings are defined in Chromium's [`hid.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/hid/hid.cc) and [`hid_device.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/hid/hid_device.cc) (chromium.googlesource.com).
:::

## Examples

Each example checks for `navigator.hid` first and states what the page does on Firefox, Safari, or Android, where the property is absent.

### Pairing a vendor-specific device and opening it

Filter on the vendor ID so the chooser lists only that manufacturer's devices, take the first granted device, and open it. The call must sit inside the `click` handler. Without WebHID the handler shows a message pointing to the manufacturer's desktop utility instead.

```js
const LOGITECH = 0x046d;

document.querySelector('#pair').addEventListener('click', async () => {
  const note = document.querySelector('#hid-note');
  if (!('hid' in navigator)) {
    note.textContent = 'This browser cannot talk to HID devices; use the vendor utility to configure it.';
    return;
  }

  const [device] = await navigator.hid.requestDevice({ filters: [{ vendorId: LOGITECH }] });
  if (!device) return; // chooser dismissed: an empty array, not an error

  if (!device.opened) await device.open();
  note.textContent = `Connected to ${device.productName} (${device.vendorId.toString(16)}:${device.productId.toString(16)})`;
});
```

`requestDevice()` resolves with an empty array when the user closes the chooser, so destructuring and testing `device` covers the cancel case without a `try`/`catch`.

### Reading input reports from a custom controller

After `open()`, each report the device sends arrives as an `inputreport` event with a `DataView` of the payload. The handler below decodes a two-byte vendor report into a dial value and a button bitmap; the layout comes from the device's report descriptor, which `device.collections` exposes if you need to inspect it at runtime.

```js
function listenToDial(device, onChange) {
  if (!('hid' in navigator)) return () => {};

  const handler = (event) => {
    if (event.reportId !== 0x01) return;
    const position = event.data.getUint8(0);
    const buttons = event.data.getUint8(1);
    onChange({ position, pressed: (buttons & 0x01) !== 0 });
  };
  device.addEventListener('inputreport', handler);
  return () => device.removeEventListener('inputreport', handler);
}
```

`event.data` excludes the report ID byte; it is delivered separately as `event.reportId`, which is `0` on devices whose interface does not number its reports.

### Reconnecting granted devices on page load

Permission survives reloads, so `getDevices()` returns previously granted devices without a prompt and without needing a gesture. Combine it with the `connect` and `disconnect` events to reopen a device that is unplugged and plugged back in while the page is open.

```js
async function restoreDevices(onDevice, onGone) {
  if (!('hid' in navigator)) return;

  for (const device of await navigator.hid.getDevices()) {
    await device.open();
    onDevice(device);
  }
  navigator.hid.addEventListener('connect', async ({ device }) => {
    await device.open();
    onDevice(device);
  });
  navigator.hid.addEventListener('disconnect', ({ device }) => onGone(device));
}
```

`forget()` revokes the grant from script, so a "Remove device" button can clear it without sending the user to the site-settings page.

## See also

- [WebUSB API](/reference/capabilities/web-usb/), for USB interfaces that are not HID class
- [Web Serial API](/reference/capabilities/web-serial/)
- [Web Bluetooth API](/reference/capabilities/web-bluetooth/), for Bluetooth Low Energy devices that use GATT instead of HID
- [WebHID: requestDevice() method](https://hid.spec.whatwg.org/#dom-hid-requestdevice) (hid.spec.whatwg.org)
- [WebHID: HIDDevice open() method](https://hid.spec.whatwg.org/#dom-hiddevice-open) (hid.spec.whatwg.org)
- [WebKit standards position: WebHID](https://github.com/WebKit/standards-positions/issues/510) (github.com)
- [Connecting to uncommon HID devices](https://developer.chrome.com/docs/capabilities/hid) (developer.chrome.com)