loading
Warning:
Chromium only, over HTTPS WebHID ships in Chromium-based browsers. Firefox and Safari do not implement it. RequestDevice must run inside a user gesture.
Note:
Not for keyboards and mice The browser blocks the usage pages that would let a page impersonate the user's own input - keyboards and mice among them. WebHID is for the long tail, and what a report's bytes mean comes from the device's own documentation, not from this API.

Support check

IsSupported

True when the runtime exposes navigator.hid. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.

C#
@inject Bit.Butil.Hid hid

var supported = await hid.IsSupported();
Live sample
support output
Results will appear here when you interact with the samples.

Pick a device

RequestDevice / GetDevices

RequestDevice returns an array, because one physical device can present as several collections and the chooser can hand back more than one. GetDevices returns what this origin was already granted, without a prompt.

C#
private HidDevice? _device;

var devices = await hid.RequestDevice(new HidDeviceFilter { VendorId = 0x054c });
_device = devices.FirstOrDefault();

var granted = await hid.GetDevices();
Live sample
Vendor id (hex, blank for any)
Usage page (hex, optional)
device output
Results will appear here when you interact with the samples.

Open and inspect

Open / Close / IsOpened / GetInfo

A device has to be opened before any report crosses. GetInfo re-reads its collections - the report ids it will accept, which is what a protocol implementation needs first.

Razor
@code {
    // The handle from RequestDevice above - a grant is per device, and this is the way back to one
    // the user has already chosen.
    private HidDevice? _device;

    private async Task Inspect()
    {
        await _device!.Open();

        var info = await _device.GetInfo();
        foreach (var collection in info!.Collections)
        {
            // collection.UsagePage, collection.Usage
            // collection.InputReports / OutputReports / FeatureReports
        }
    }
}
Live sample
no device picked yet
open output
Results will appear here when you interact with the samples.

Input reports

SubscribeInputReports

The device's unprompted events: buttons, axes, whatever its report descriptor declares. Open the device first - a closed device sends nothing.

Razor
@implements IAsyncDisposable

@code {
    private HidDevice? _device;   // from RequestDevice, and opened
    private ButilSubscription? _reports;

    private async Task Listen() =>
        _reports = await _device!.SubscribeInputReports(report =>
        {
            // report.ReportId, report.Data
        });

    public async ValueTask DisposeAsync()
    {
        if (_reports is not null) await _reports.DisposeAsync();
    }
}
Live sample
no device picked yet
input report output
Results will appear here when you interact with the samples.

Output and feature reports

SendReport / SendFeatureReport / ReceiveFeatureReport

Output reports drive the device - an LED, a rumble motor, a display. Feature reports are read and written on demand and carry configuration rather than events.

Razor
@code {
    private HidDevice? _device;   // from RequestDevice, and opened

    private async Task Send()
    {
        await _device!.SendReport(reportId: 0x01, [0x00, 0xff]);

        await _device.SendFeatureReport(reportId: 0x02, [0x01]);
        var feature = await _device.ReceiveFeatureReport(reportId: 0x02);
    }
}
Live sample
Report id
Payload (hex, space separated)
no device picked yet
report output
Results will appear here when you interact with the samples.

Plug and unplug

SubscribeConnection

Watches devices appearing and disappearing. Only devices this origin already has permission for raise these.

C#
await using var watch = await hid.SubscribeConnection(
    onConnected: device => Console.WriteLine($"+ {device.ProductName}"),
    onDisconnected: device => Console.WriteLine($"- {device.ProductName}"));
Live sample
connection output
Results will appear here when you interact with the samples.

Revoke the grant

Forget

Drops this origin's permission for the device, so it stops appearing in GetDevices until the user picks it again.

Razor
@code {
    private HidDevice? _device;   // from RequestDevice

    // Drops the grant, so GetDevices stops returning it and the picker has to be shown again.
    private async Task Revoke()
    {
        var revoked = await _device!.Forget();
        _device = null;
    }
}
Live sample
forget output
Results will appear here when you interact with the samples.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes navigator.hid. Returns default (false) during prerender/SSR instead of throwing.
RequestDevice
ValueTask<HidDevice[]> RequestDevice(params HidDeviceFilter[] filters)
Opens the device chooser and returns the picked devices - an array, since one unit can present as several. Needs a user gesture.
GetDevices
ValueTask<HidDevice[]> GetDevices()
The devices this origin has already been granted, without a prompt.
SubscribeConnection
ValueTask<ButilSubscription> SubscribeConnection(Action<HidDevice>? onConnected = null, Action<HidDevice>? onDisconnected = null)
Watches permitted devices being plugged in and unplugged.
DisposeAsync
ValueTask DisposeAsync()
Closes every device this service handed out, detaches listeners and releases the JS callback reference.
InvokeHidInputReport
void InvokeHidInputReport(Guid id, HidInputReport report)
JSInvokable interop plumbing for input reports - not intended for app code.
InvokeHidConnected
void InvokeHidConnected(Guid id, HidDeviceInfo info)
JSInvokable interop plumbing for the connect event - not intended for app code.
InvokeHidDisconnected
void InvokeHidDisconnected(Guid id, HidDeviceInfo info)
JSInvokable interop plumbing for the disconnect event - not intended for app code.
HidDevice.Info / ProductName
HidDeviceInfo Info { get; } / string? ProductName { get; }
The device as it was when the handle was created, including its collections.
HidDevice.Open / Close / IsOpened
ValueTask<bool> Open() / ValueTask Close() / ValueTask<bool> IsOpened()
Opens, closes and reports the device's open state.
HidDevice.GetInfo
ValueTask<HidDeviceInfo?> GetInfo()
Re-reads the device's state; Info is only the snapshot from when the handle was created.
HidDevice.SendReport
ValueTask<bool> SendReport(byte reportId, byte[] data)
Sends an output report.
HidDevice.SendFeatureReport / ReceiveFeatureReport
ValueTask<bool> SendFeatureReport(byte reportId, byte[] data) / ValueTask<byte[]?> ReceiveFeatureReport(byte reportId)
Writes and reads a feature report.
HidDevice.SubscribeInputReports
ValueTask<ButilSubscription> SubscribeInputReports(Action<HidInputReport> handler)
Subscribes to the device's input reports. Dispose the subscription to detach.
HidDevice.Forget
ValueTask<bool> Forget()
Revokes this origin's permission for the device.
HidDevice.DisposeAsync
ValueTask DisposeAsync()
Closes the device, detaches its listeners and releases the browser-side reference.
HidDeviceFilter
class HidDeviceFilter { ushort? VendorId; ushort? ProductId; ushort? UsagePage; ushort? Usage; }
One chooser filter.
HidDeviceInfo
class HidDeviceInfo { string Id; ushort VendorId; ushort ProductId; string? ProductName; bool Opened; HidCollectionInfo[] Collections; }
A granted device and its top-level collections.
HidCollectionInfo / HidReportInfo
class HidCollectionInfo { ushort UsagePage; ushort Usage; HidReportInfo[] InputReports; HidReportInfo[] OutputReports; HidReportInfo[] FeatureReports; }
One logical device inside the physical one, and the reports it declares.
HidInputReport
class HidInputReport { byte ReportId; byte[] Data; }
One input report: its id and its payload, without the leading id byte.
An unhandled error has occurred. Reload 🗙