# Web Serial API: navigator.serial device access

> How navigator.serial.requestPort() and getPorts() grant page access to a serial port, which browsers support it, and how to feature-detect it with a fallback.

import CompatTable from '@components/CompatTable.astro';

**In one line:** the Web Serial API lets a page ask the user to pick a serial port with
`navigator.serial.requestPort()`, then read and write bytes through the returned
`SerialPort`'s `readable`/`writable` streams. MDN marks the `Serial` interface
**not Baseline** — "because it does not work in some of the most widely-used browsers" —
and exposes it only in **secure contexts**.

## Requesting and reusing a port

```js
const port = await navigator.serial.requestPort();
await port.open({ baudRate: 9600 });
```

Per MDN, `requestPort()` "must be called via transient activation" — trigger it from a
click handler or similar user gesture, not automatically on load. `navigator.serial.getPorts()`
returns an array of the currently connected ports the origin already has permission to
access, without prompting again:

```js
const ports = await navigator.serial.getPorts();
```

Access can also be blocked by the `serial` Permissions Policy; per MDN, a blocked page
never shows the device picker and the user is not prompted.

## Where it is supported

<CompatTable feature="web-serial" />

Per the compatibility data above, desktop Chrome and Edge have supported the API since
version 89, and desktop Firefox added support later, at version 151. Safari does not
support it on macOS or iOS. On Android, Chrome has full support from version 148 per MDN
browser-compat-data; in Chrome 138-146 serial ports there were "only available if they are
provided by Bluetooth RFCOMM serial port emulation". Firefox for Android does not support it.

## Reading and writing

```js
const reader = port.readable.getReader();
try {
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    console.log(value);
  }
} finally {
  reader.releaseLock();
}
```

`navigator.serial` also fires `connect` and `disconnect` events — bubbling from
`SerialPort` — so a page can react when an already-authorized device is attached or
removed instead of polling `getPorts()`.

## How to detect it at runtime

```js
async function connectSerial() {
  if (!('serial' in navigator)) {
    // Web Serial unsupported here — fall back to WebUSB, or point the user at a
    // native companion app instead of calling navigator.serial.
    return null;
  }
  const port = await navigator.serial.requestPort();
  await port.open({ baudRate: 9600 });
  return port;
}
```

## What goes wrong

- [ ] `requestPort()` requires transient activation — call it from a click handler, not
      automatically on page load, or the returned promise rejects.
- [ ] Feature-detect `'serial' in navigator` before calling anything on it; MDN marks Web
      Serial not Baseline, so absence is expected in some browsers.
- [ ] Serve the page over HTTPS — the `Serial` interface is exposed only in secure
      contexts.
- [ ] On Android, check the Chrome version before assuming USB-connected devices are reachable —
      per MDN browser-compat-data, Chrome 138-146 covered Bluetooth RFCOMM emulation only; 148+ is full.
- [ ] Call `getPorts()` on load, before falling back to `requestPort()`, to reuse a
      previously granted port without a new prompt.
- [ ] A page can also be blocked from prompting at all by the `serial` Permissions
      Policy — check for that failure mode separately from a user simply declining.

## Where to go next

- [WebUSB API](/reference/capabilities/web-usb/) — the related reference for USB devices
  not claimed by an OS driver.
- [WebHID API](/reference/capabilities/web-hid/) — the related reference for USB HID
  class input devices.