# EyeDropper API

> new EyeDropper().open() samples the colour of any pixel on screen for a custom colour picker. The signal option, every exception it rejects with, fallbacks.

The EyeDropper API gives a page the browser's own eyedropper tool: `new EyeDropper().open()` suppresses input to the page, lets the user pick one pixel anywhere on screen, including outside the browser window, and resolves with that pixel's colour as a `#rrggbb` string. It exists so custom colour pickers do not have to screenshot the page to sample a colour.

Chrome 95 and Edge 95 ship it on desktop only; BCD records `version_added: false` for Chrome on Android and Android WebView. Firefox and Safari have no implementation (BCD `api.EyeDropper`). Chrome 96 moved the interface behind `[SecureContext]`, so on `http://` origins other than `localhost` `window.EyeDropper` is `undefined`.

## Syntax

```js
const eyeDropper = new EyeDropper();

eyeDropper.open()
eyeDropper.open(options)
```

The constructor takes no arguments and does nothing visible. `open()` returns a `Promise<ColorSelectionResult>`; the result's only member is `sRGBHex`, a `DOMString` holding a valid simple colour such as `"#3366ff"`. While the promise is pending the page is in "eyedropper mode" and receives no UI events. `open()` must be called with transient user activation, so it belongs inside a `click` or `keydown` handler.

## Parameters

`open()` takes one optional `options` argument, a `ColorSelectionOptions` dictionary with a single member.

| Member | Type | Required | Description |
|---|---|---|---|
| `signal` | `AbortSignal` | No | Aborting the signal exits eyedropper mode, dismisses the browser UI, and rejects the pending promise with the signal's abort reason (an `AbortError` `DOMException` unless `abort()` was given another reason). |

The resolved `ColorSelectionResult` dictionary has one member, `sRGBHex`, a six-digit `#rrggbb` string. The specification exposes no alpha channel, so a translucent pixel is reported as the colour composited on screen.

## Exceptions

`open()` rejects its promise with one of the following. The first two are checked synchronously before any UI appears.

| Exception | Condition |
|---|---|
| `NotAllowedError` | The window has no transient user activation. |
| `InvalidStateError` | Another eyedropper is already open in this window. |
| `AbortError` | The user cancelled the selection (Escape in Chrome) before picking a pixel. |
| `OperationError` | The browser could not present its UI or could not read screen content; Chrome also uses it when the feature is disabled by policy. |
| The signal's abort reason | `options.signal` was already aborted when `open()` was called, or was aborted while the promise was pending. |

:::observed
Chrome rejects `open()` called from a `setTimeout` callback with `NotAllowedError: EyeDropper::open() requires user gesture.`, a second `open()` while the first is pending with `InvalidStateError: EyeDropper is already open.`, and a pick cancelled with Escape with `AbortError: The user canceled the selection.`; when the eyedropper is unavailable it rejects with `OperationError: EyeDropper is not available.` All four strings are in Chromium's [`eye_dropper.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/eyedropper/eye_dropper.cc) (chromium.googlesource.com).
:::

## Examples

Both examples test `"EyeDropper" in window` before showing any pick button and keep an `<input type="color">` in the page as the path for Firefox, Safari, and every mobile browser.

### Picking a colour with a native colour input as the fallback

When the constructor is missing, hide the pick button and leave the colour input visible; the input gives every browser a working colour picker, just not one that can sample pixels outside the page.

```js
const pickButton = document.querySelector("#pick");
const colorInput = document.querySelector("#color");

if (!("EyeDropper" in window)) {
  pickButton.hidden = true;
  colorInput.hidden = false;
} else {
  pickButton.addEventListener("click", async () => {
    const eyeDropper = new EyeDropper();
    try {
      const { sRGBHex } = await eyeDropper.open();
      colorInput.value = sRGBHex;
    } catch (err) {
      if (err.name !== "AbortError") console.error(`${err.name}: ${err.message}`);
    }
  });
}
```

`AbortError` is the normal outcome when the user presses Escape, so the handler ignores it and surfaces only the other names.

### Cancelling a pick from a timeout or a second button

Pass an `AbortSignal` when a pick should not stay open indefinitely. `AbortSignal.timeout()` rejects with a `TimeoutError` reason, which the `catch` branch distinguishes from the user's own cancel.

```js
async function pickWithTimeout(ms) {
  if (!("EyeDropper" in window)) return null;

  const eyeDropper = new EyeDropper();
  try {
    const { sRGBHex } = await eyeDropper.open({ signal: AbortSignal.timeout(ms) });
    return sRGBHex;
  } catch (err) {
    if (err.name === "TimeoutError") console.info("Eyedropper closed after timeout");
    return null;
  }
}

document.querySelector("#pick").addEventListener("click", async () => {
  const hex = await pickWithTimeout(10_000);
  if (hex) document.documentElement.style.setProperty("--accent", hex);
});
```

A `null` return covers unsupported browsers, timeouts, and user cancellation alike, so the caller needs one branch rather than three.

## See also

- [Async Clipboard API](/reference/capabilities/clipboard/), for copying the sampled value out of the page
- [Local Font Access API](/reference/capabilities/local-font-access/), the other desktop-only Chromium creative-tools API
- [Screen Capture API](/reference/capabilities/screen-capture/), the heavier way to read pixels outside the page
- [EyeDropper API](https://wicg.github.io/eyedropper-api/) (wicg.github.io)
- [Picking colors of any pixel on the screen with the EyeDropper API](https://developer.chrome.com/docs/capabilities/web-apis/eyedropper) (developer.chrome.com)