JavaScript EyeDropper Table
| Piece | What it does | Field note |
|---|---|---|
new EyeDropper() | The picker handle | stateless constructor - it holds nothing; make one per session and gate the whole UI on EyeDropper in window |
ed.open() | The one method | takes over with a browser-owned magnifier across the entire screen; must run from a user gesture (transient activation) or it will not start |
{sRGBHex} | The result field | the promise resolves once, to one lowercase hex string like #ffa500 - opaque, no alpha, no color-space tag |
open({signal}) | The cancel wire | optional AbortSignal; ctrl.abort() closes the native overlay and rejects the promise - the only programmatic exit |
AbortError | The normal exit | the user pressed Escape or your code aborted - catch it as a branch of the flow, not an exception in telemetry |
Chromium only | The platform gate | no Firefox, no Safari, secure context required; the constructor line itself throws where unsupported |
one pixel, sRGB | The sampling truth | a single screen pixel, no region averaging, quantized down to sRGB even on P3 displays, and no coordinates of where the user picked |
input type=color | The fallback lane | universal native picker - its dropdown carries an eyedropper on several platforms, zero prompts, works in every browser |
EyeDropper is the smallest useful API on the platform: it samples one pixel of the screen and hands you the color. The whole dance is two lines - new EyeDropper(), then const {sRGBHex} = await ed.open() - and between those lines the browser takes over: a magnifier overlay follows the pointer across the entire display, the user positions it, clicks, and your promise resolves with a lowercase hex string like #ffa500. No canvas to own, no image to upload, no permissions dialog ladder - just the one gesture that opened it, which is exactly the point: the API was designed so that screen-pixel access only ever happens behind a visible, deliberate user action.
Bottom line: one method, one result field, one cancel wire. open() must be called from a user gesture (the platform requires transient activation, so wiring it to a button click is the correct and only shape); it returns a promise resolving to {sRGBHex: '#rrggbb'} - a single object with a single field, opaque hex, no alpha channel, no coordinates of where on screen the user actually picked. Cancellation is first-class: pass open({signal: controller.signal}) and your own cancel button can abort the picker, rejecting with AbortError - the same rejection the user produces by pressing Escape. Catch AbortError as a normal branch of your UI flow (the user changed their mind), not as an exception to log; that single distinction separates polished picker UIs from console-error archaeology.
The boundaries, stated plainly: Chromium-only - Firefox and Safari do not ship it, so 'EyeDropper' in window is your feature detect and <input type=color> is the honest fallback (several native pickers carry a built-in eyedropper of their own). The sample is one screen pixel, not a region average, and it arrives in sRGB - no Display-P3 wide gamut, which matters if you are matching colors for print or wide-gamut design work. And because the API can read any pixel on the user's screen, browsers treat it as a sensitive surface: gesture-gated, secure-context-only, and trivially rejectable. For colors that already live inside your page, skip it - canvas getImageData or getComputedStyle have no prompt and no gesture tax; EyeDropper earns its keep for everything outside your DOM: design files in another window, a photo in the local viewer, a brand color on any website.
How to use
- Wire it to a button and treat dismissal as flow: btn.addEventListener('click', async () => { try { const {sRGBHex} = await new EyeDropper().open(); applyColor(sRGBHex); } catch (err) { if (err.name === 'AbortError') return; /* user dismissed - stay quiet */ throw err; } });
- Add a real cancel button with a signal: const ctrl = new AbortController(); const result = ed.open({signal: ctrl.signal}); cancelBtn.onclick = () => ctrl.abort(); - without a signal, Escape is the only way out and your pending promise just waits.
- Feature-detect and fall back cleanly: if (!('EyeDropper' in window)) reveal a hidden <input type=color> instead - the native picker ships an eyedropper mode on several platforms, and the input works in every browser ever made.
- Parse the hex once into components: const n = parseInt(sRGBHex.slice(1), 16); then r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255 - or template it straight into rgb() / CSS custom properties; sRGBHex is always 7 characters of lowercase hex with no alpha.
- Sample inside your own page without the API at all: ctx.getImageData(x, y, 1, 1).data for canvas pixels, getComputedStyle(el).color for rendered styles - no gesture, no prompt, and none of the Chromium-only boundary; reserve EyeDropper for pixels your document does not own.
Frequently asked questions
Why does open() demand a click, and what is the browser actually showing my user?
The gesture requirement (transient activation) is the privacy model, not a formality: EyeDropper can read the color of any pixel on the screen, which includes things the user never gave your page - a document in another window, a photo in a local viewer, a password hint scrawled in a notes app. Browsers close that hole by making screen sampling impossible to trigger from script alone: a real, recent user click must hand off to open(), and what appears is a browser-owned magnifier overlay that follows the pointer across the whole display until the user clicks a pixel or presses Escape. Your page stays in the dark about the journey: no events during picking, no coordinates in the result, just the final hex. The design consequence for your UI: never attempt to call open() outside a handler (it will not work), do not build flows that batch-sample multiple colors without re-clicking (each pick is its own gesture), and do not treat the magnifier as skinnable or brandable - it is the browser vouching for the interaction, which is precisely why users trust it. The same logic explains the secure-context rule: an API that can silently observe screen colors has no business running over plain HTTP.
How do I cancel the picker programmatically - and what is the right way to handle AbortError?
Pass an AbortSignal: const ctrl = new AbortController(); ed.open({signal: ctrl.signal}). Calling ctrl.abort() while the magnifier is up closes the picker and rejects the promise with an AbortError DOMException. This is the difference between a cancel button that works and one that lies: without a signal, your only exits are the user clicking a color or pressing Escape - your in-page cancel button can close its own modal chrome, but the native overlay stays up and the promise stays pending. Handle the rejection as control flow, not failure: AbortError means a human looked at the picker and declined - return quietly, restore whatever button state you parked, and maybe nudge nothing at all. The anti-pattern is dumping it into your global error handler, where a user pressing Escape on a color picker starts looking like an exception storm in your telemetry. One more branch worth distinguishing: if the environment lacks EyeDropper entirely, the constructor line itself throws a TypeError - feature-detect with 'EyeDropper' in window before wiring handlers so AbortError stays reserved for real dismissals and your fallback path owns the unsupported case.
What exactly comes back in sRGBHex - and what information is deliberately missing?
You get one object with one field: {sRGBHex: '#rrggbb'} - lowercase, six hex digits, always opaque, no alpha (a screen pixel has no transparency to sample; the compositor already flattened it), and no color-space tag: values are sRGB, full stop. What is deliberately absent: the pick coordinates - the API never tells you where on the screen the user clicked, only what color was there, which kills an entire class of screen-scraping uses by design; a region sample - it is one pixel, so a dithered gradient or a JPEG edge gives you a single noisy sample rather than an average; and any wide-gamut value - on a Display-P3 monitor the magnifier shows you the pixel but the answer is quantized down to sRGB, so out-of-gamut neon crushes toward the nearest encodable value. Practical workarounds that respect the design: for stability on noisy edges, have the user pick once, then re-sample near that point in your own canvas if the content is yours; for color management, treat the result as a design-system input (someone picks the color, your toolchain stores it) rather than a real-time measurement device; and if you need P3 math, that is a canvas/display-p3 conversation, not an EyeDropper one.
When should I skip EyeDropper entirely, and what do the alternatives do better?
Four cases, each with a named alternative. Colors inside your own document: getComputedStyle(element).color reads any rendered style with zero prompts, and canvasRenderingContext.getImageData(x, y, 1, 1) samples your own canvas pixels - both work in every browser, no gesture tax, exact values including alpha. Your page displays the image being sampled: draw it to a canvas (same-origin or CORS-clean only - tainted canvases block getImageData) and read pixels directly with hover previews, which is a far better UX than the system magnifier anyway. Cross-browser support is the requirement: <input type=color> is universal, and on Windows, macOS and ChromeOS builds its native dropdown often includes an eyedropper - detect support, and on Firefox or Safari the input is simply the correct answer. High-frequency or automated sampling: EyeDropper is gesture-gated per pick by law-of-the-platform, so any flow needing dozens of samples (batch palette extraction, thumbnail analysis) belongs in canvas or a Worker with ImageBitmap, not in a magnifier loop. The honest sweet spot for the API is precisely the rest of the screen: pick the brand color from a PDF in another window, match a paint chip from a photo in the local viewer, steal a hue from any web page - one click, one pixel, done.