Introduction to the EyeDropper API
The EyeDropper API is a modern browser API that allows web developers to build color sampling tools directly into their web applications. It provides a way for users to pick a color from any pixel on their screen — including areas outside the browser window — and return that color value to the application as a hex color string.
Think of it as the digital equivalent of the eyedropper tool found in graphic design software like Photoshop, Figma, or GIMP, but now available natively in the browser without requiring any third-party libraries or plugins.
What Exactly Is the EyeDropper API?
The API exposes a single interface: the EyeDropper object, which lives on the window namespace. When invoked, it triggers a system-level color picker that lets the user sample a color from anywhere on their display. The sampled color is returned as a hexadecimal color string in the sRGB color space — for example, #FF6B35.
Under the hood, the browser communicates with the operating system's native color sampling capabilities, presenting a magnified crosshair or loupe interface that helps the user precisely target the pixel they want to sample. Once the user clicks or taps, the color value is captured and returned to your JavaScript code.
Why the EyeDropper API Matters
Before this API existed, developers who wanted color-picking functionality had to resort to cumbersome workarounds. These included embedding a hidden <canvas> element with a screenshot rendered to it, using drag-and-drop hacks, or relying on heavyweight third-party libraries that often required user permission dialogs for screen capture. The EyeDropper API eliminates all of that friction.
Here's why it's significant:
- Native performance and UX — The browser delegates the actual sampling to the operating system, resulting in a smooth, familiar experience that matches the platform's native eyedropper tool.
- Cross-screen sampling — Unlike canvas-based approaches that are confined to the browser viewport, the EyeDropper API can sample colors from anywhere on the user's entire screen, including other applications and the desktop background.
- Minimal permissions friction — No persistent permission grants are required. The API uses a transient activation model tied to a specific user gesture, which respects user privacy and avoids intrusive permission dialogs.
- Precise color accuracy — The returned hex value represents the exact screen-rendered color at the sampled pixel, without any lossy conversions or approximations.
- Lightweight integration — You get all of this with just a few lines of vanilla JavaScript, with no dependencies, no build steps, and no large library payloads.
Browser Support and Feature Detection
As of 2025, the EyeDropper API is supported in Chromium-based browsers including Google Chrome, Microsoft Edge, Opera, and Brave. It is not yet available in Firefox or Safari. Always perform feature detection before using the API to avoid runtime errors in unsupported browsers.
if ('EyeDropper' in window) {
// The API is supported — safe to use
console.log('EyeDropper API is available');
} else {
// Fall back to an alternative approach or show a message
console.warn('EyeDropper API is not supported in this browser');
}
This simple guard ensures your application degrades gracefully when the API is absent.
How to Use the EyeDropper API
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →The EyeDropper Interface
The entire API surface is elegantly minimal. The EyeDropper constructor takes no arguments and exposes a single method:
new EyeDropper()— Creates a new instance of the eyedropper tool.eyeDropper.open()— Opens the system-level color picker. Returns a Promise that resolves with aColorSelectionResultobject or rejects if the user cancels or an error occurs.
The ColorSelectionResult object contains a single property:
sRGBHex— A string representing the selected color in hexadecimal sRGB notation, including the leading#(for example,"#AABBCC").
Opening the Eyedropper — Basic Example
Here is the simplest possible usage. A button click triggers the eyedropper, and the selected color is logged to the console:
<button id="pick-color-btn">Pick a color from the screen</button>
<script>
const pickButton = document.getElementById('pick-color-btn');
pickButton.addEventListener('click', async () => {
if (!('EyeDropper' in window)) {
alert('Your browser does not support the EyeDropper API.');
return;
}
const eyeDropper = new EyeDropper();
try {
const result = await eyeDropper.open();
console.log('Selected color:', result.sRGBHex);
// result.sRGBHex will be something like "#FF6B35"
} catch (error) {
// User cancelled or an error occurred
console.log('Color selection was cancelled or failed:', error);
}
});
</script>
This example covers the essential pattern: create an instance, call open() inside an async function triggered by a user gesture, await the result, and handle both the success and error cases.
Critical Rule: User Activation Required
The open() method must be called from within a transient user activation event — meaning a click, tap, or key press initiated by the user. If you try to call open() programmatically without a recent user gesture (for example, from a setTimeout callback or on page load), the browser will throw a NotAllowedError DOMException.
// ❌ This will FAIL — no user gesture
setTimeout(async () => {
const dropper = new EyeDropper();
await dropper.open(); // Throws NotAllowedError
}, 1000);
// ✅ This works — triggered by a button click
button.addEventListener('click', async () => {
const dropper = new EyeDropper();
await dropper.open(); // Allowed
});
This security restriction prevents websites from silently sampling screen colors without the user's explicit consent and awareness.
Handling Errors Gracefully
The open() Promise can reject with several different error types, and it's important to distinguish between them for a polished user experience:
async function pickColor() {
const eyeDropper = new EyeDropper();
try {
const result = await eyeDropper.open();
return result.sRGBHex;
} catch (error) {
switch (error.name) {
case 'AbortError':
// The user dismissed the eyedropper UI (pressed Escape or clicked away)
console.log('User cancelled color selection');
break;
case 'NotAllowedError':
// open() was called without a user gesture or the gesture expired
console.error('EyeDropper requires a recent user activation');
break;
case 'SecurityError':
// The page context doesn't have permission (e.g., in an iframe without allow)
console.error('Security restrictions prevented eyedropper usage');
break;
default:
console.error('Unexpected error:', error.message);
}
return null;
}
}
The most common rejection is AbortError, which simply means the user changed their mind and dismissed the picker. Treat this as a normal, expected outcome rather than an actual error.
Practical Code Examples
Building a Full Color Picker Widget
Let's build a complete, self-contained color picker component that displays the selected color, updates a preview swatch, and copies the hex value to the clipboard. This is the kind of widget you'd integrate into a design tool, a theme customizer, or a developer utilities panel.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EyeDropper Color Picker</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #f5f5f5;
}
.color-picker-card {
background: white;
border-radius: 16px;
padding: 32px;
box-shadow: 0 4px 24px rgba(0,0,0,0.1);
width: 360px;
text-align: center;
}
.color-swatch {
width: 120px;
height: 120px;
border-radius: 50%;
margin: 0 auto 20px;
border: 4px solid #e0e0e0;
transition: background-color 0.2s ease;
background-color: #cccccc;
}
.color-value {
font-size: 1.5rem;
font-weight: 600;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
margin-bottom: 8px;
color: #333;
}
.color-label {
font-size: 0.85rem;
color: #888;
margin-bottom: 24px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.1s, box-shadow 0.1s;
}
.btn:active { transform: scale(0.97); }
.btn-primary {
background: #4f46e5;
color: white;
margin-right: 8px;
}
.btn-primary:hover { background: #4338ca; box-shadow: 0 2px 8px rgba(79,70,229,0.4); }
.btn-secondary {
background: #e5e7eb;
color: #374151;
}
.btn-secondary:hover { background: #d1d5db; }
.copied-toast {
display: inline-block;
background: #10b981;
color: white;
padding: 6px 14px;
border-radius: 20px;
font-size: 0.85rem;
margin-top: 12px;
opacity: 0;
transition: opacity 0.3s;
}
.copied-toast.visible { opacity: 1; }
</style>
</head>
<body>
<div class="color-picker-card">
<h2 style="margin-bottom:20px;color:#1f2937;">Screen Color Picker</h2>
<div class="color-swatch" id="swatch"></div>
<p class="color-value" id="colorValue">No color picked</p>
<p class="color-label">Click the button below and pick any color from your screen</p>
<div>
<button class="btn btn-primary" id="pickBtn">🎨 Pick Color</button>
<button class="btn btn-secondary" id="copyBtn" disabled>📋 Copy Hex</button>
</div>
<span class="copied-toast" id="toast">Copied!</span>
</div>
<script>
(() => {
const swatch = document.getElementById('swatch');
const colorValueEl = document.getElementById('colorValue');
const pickBtn = document.getElementById('pickBtn');
const copyBtn = document.getElementById('copyBtn');
const toast = document.getElementById('toast');
let currentColor = null;
// Feature detection
const isSupported = 'EyeDropper' in window;
if (!isSupported) {
pickBtn.disabled = true;
pickBtn.textContent = '❌ Not Supported';
pickBtn.title = 'EyeDropper API is not available in this browser';
colorValueEl.textContent = 'Browser not supported';
colorValueEl.style.color = '#ef4444';
}
pickBtn.addEventListener('click', async () => {
if (!isSupported) return;
const eyeDropper = new EyeDropper();
try {
const result = await eyeDropper.open();
currentColor = result.sRGBHex;
// Update the UI
swatch.style.backgroundColor = currentColor;
colorValueEl.textContent = currentColor;
colorValueEl.style.color = '#1f2937';
copyBtn.disabled = false;
// Optional: adjust text color on swatch for contrast
// (simple luminance check)
const r = parseInt(currentColor.slice(1,3), 16);
const g = parseInt(currentColor.slice(3,5), 16);
const b = parseInt(currentColor.slice(5,7), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
if (luminance < 0.5) {
swatch.style.borderColor = '#ffffff';
} else {
swatch.style.borderColor = '#e0e0e0';
}
} catch (err) {
if (err.name === 'AbortError') {
// User cancelled — no action needed
console.log('User dismissed the eyedropper');
} else {
console.error('EyeDropper error:', err);
alert('Could not open the color picker: ' + err.message);
}
}
});
copyBtn.addEventListener('click', async () => {
if (!currentColor) return;
try {
await navigator.clipboard.writeText(currentColor);
toast.classList.add('visible');
setTimeout(() => toast.classList.remove('visible'), 2000);
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = currentColor;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
toast.classList.add('visible');
setTimeout(() => toast.classList.remove('visible'), 2000);
}
});
})();
</script>
</body>
</html>
This complete widget demonstrates several important patterns: feature detection with graceful degradation, user gesture handling, error type discrimination, clipboard integration, and dynamic UI updates based on the selected color. You can drop this entire file into a browser that supports the API and it will work immediately.
Integrating with a Canvas Drawing Application
The EyeDropper API is particularly valuable in creative tools. Here's an example that integrates the eyedropper into a simple canvas drawing app, allowing the user to sample colors from anywhere on screen and then paint with them:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Canvas + EyeDropper</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; }
canvas { border: 2px solid #ccc; border-radius: 8px; cursor: crosshair; }
.toolbar { margin-bottom: 16px; display: flex; gap: 10px; align-items: center; }
.color-indicator {
width: 32px; height: 32px; border-radius: 6px;
border: 2px solid #999; display: inline-block;
background-color: #000000;
}
button { padding: 8px 16px; cursor: pointer; }
</style>
</head>
<body>
<div class="toolbar">
<span>Brush Color:</span>
<span class="color-indicator" id="brushPreview"></span>
<span id="brushHex" style="font-family:monospace;">#000000</span>
<button id="eyedropBtn">💧 Sample Screen Color</button>
<button id="clearBtn">Clear Canvas</button>
</div>
<canvas id="canvas" width="600" height="400"></canvas>
<script>
(() => {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const brushPreview = document.getElementById('brushPreview');
const brushHex = document.getElementById('brushHex');
const eyedropBtn = document.getElementById('eyedropBtn');
const clearBtn = document.getElementById('clearBtn');
// Set initial canvas background
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
let brushColor = '#000000';
let isDrawing = false;
function updateBrushUI(color) {
brushColor = color;
brushPreview.style.backgroundColor = color;
brushHex.textContent = color;
}
// Drawing logic
canvas.addEventListener('mousedown', (e) => {
isDrawing = true;
ctx.beginPath();
ctx.moveTo(e.offsetX, e.offsetY);
});
canvas.addEventListener('mousemove', (e) => {
if (!isDrawing) return;
ctx.strokeStyle = brushColor;
ctx.lineWidth = 4;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
});
canvas.addEventListener('mouseup', () => { isDrawing = false; });
canvas.addEventListener('mouseleave', () => { isDrawing = false; });
// Clear canvas
clearBtn.addEventListener('click', () => {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
});
// EyeDropper integration
eyedropBtn.addEventListener('click', async () => {
if (!('EyeDropper' in window)) {
alert('EyeDropper API is not supported in your browser.');
return;
}
const eyeDropper = new EyeDropper();
try {
const result = await eyeDropper.open();
updateBrushUI(result.sRGBHex);
console.log('Brush color updated to:', result.sRGBHex);
} catch (err) {
if (err.name === 'AbortError') {
console.log('Sampling cancelled by user');
} else {
console.error('Sampling error:', err);
}
}
});
})();
</script>
</body>
</html>
This integration shows how naturally the EyeDropper API fits into creative workflows. The user can draw on the canvas, then click the eyedropper button to sample any color from their screen — perhaps from a reference image open in another window, a design mockup, or a color palette website — and immediately continue painting with that sampled color.
Building a Color History Palette
Professional design tools typically maintain a history of recently sampled colors. Here's how to implement that pattern alongside the EyeDropper API:
<div id="color-history">
<h3>Color History</h3>
<div id="historySlots" style="display:flex;gap:8px;flex-wrap:wrap;"></div>
</div>
<script>
(() => {
const historySlots = document.getElementById('historySlots');
const MAX_HISTORY = 12;
let colorHistory = JSON.parse(localStorage.getItem('colorHistory') || '[]');
function renderHistory() {
historySlots.innerHTML = '';
colorHistory.forEach((color, index) => {
const slot = document.createElement('div');
slot.style.cssText = `
width: 36px; height: 36px; border-radius: 6px;
background-color: ${color}; cursor: pointer;
border: 2px solid #ccc; transition: transform 0.15s;
`;
slot.title = color + ' — Click to set as active';
slot.addEventListener('click', () => {
// Set this color as the currently active one
document.dispatchEvent(new CustomEvent('colorSelected', { detail: color }));
});
slot.addEventListener('mouseenter', () => {
slot.style.transform = 'scale(1.15)';
});
slot.addEventListener('mouseleave', () => {
slot.style.transform = 'scale(1)';
});
historySlots.appendChild(slot);
});
// Add an empty "pick new" slot at the end
const pickSlot = document.createElement('div');
pickSlot.style.cssText = `
width: 36px; height: 36px; border-radius: 6px;
border: 2px dashed #aaa; cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 18px; color: #888;
`;
pickSlot.innerHTML = '💧';
pickSlot.title = 'Sample a new color';
pickSlot.addEventListener('click', async () => {
if (!('EyeDropper' in window)) return;
const eyeDropper = new EyeDropper();
try {
const result = await eyeDropper.open();
addToHistory(result.sRGBHex);
} catch (err) {
// handle cancellation
}
});
historySlots.appendChild(pickSlot);
}
function addToHistory(hexColor) {
// Remove duplicate if exists
colorHistory = colorHistory.filter(c => c !== hexColor);
// Add to front
colorHistory.unshift(hexColor);
// Trim to max
if (colorHistory.length > MAX_HISTORY) {
colorHistory = colorHistory.slice(0, MAX_HISTORY);
}
// Persist
localStorage.setItem('colorHistory', JSON.stringify(colorHistory));
renderHistory();
}
renderHistory();
})();
</script>
This pattern persists the color history in localStorage, so sampled colors survive page reloads. Each history slot is interactive — clicking a stored color dispatches a custom event you can hook into, and clicking the dashed eyedropper slot triggers a new sampling session.
Best Practices
Always Respect the User Activation Constraint
The single most common pitfall with the EyeDropper API is attempting to call open() outside the context of a user gesture. Design your application flow so that the eyedropper is always triggered by a deliberate user action — a button click, a keyboard shortcut tied to a keypress event, or a menu item selection. Never attempt to open it on page load, from a timer callback, or from an asynchronous chain that loses the activation context.
If you need to chain operations after opening the eyedropper, keep the async function within the event handler scope:
// ✅ Correct: entire flow stays within the click handler's async context
button.addEventListener('click', async () => {
const dropper = new EyeDropper();
try {
const result = await dropper.open();
// Now you can do additional async work — the activation already served its purpose
await fetch('/api/colors', {
method: 'POST',
body: JSON.stringify({ color: result.sRGBHex })
});
updateUI(result.sRGBHex);
} catch (err) {
handleError(err);
}
});
// ❌ Incorrect: trying to open from a detached context
button.addEventListener('click', () => {
// This loses the user activation after the setTimeout
setTimeout(async () => {
const dropper = new EyeDropper();
await dropper.open(); // Will throw NotAllowedError
}, 100);
});
Provide a Robust Fallback Strategy
Since the EyeDropper API is not universally available across all browsers, always implement a fallback. There are several viable alternatives depending on your application's requirements:
- Input type="color" fallback — The simplest fallback is to show a native
<input type="color">element when the API is unavailable. This doesn't allow sampling from outside the browser, but it provides a functional color picker with zero implementation effort. - Canvas-based screenshot sampling — For applications that need true screen sampling, you can combine the Screen Capture API (
getDisplayMedia) with a canvas to let users capture a screen region and then sample colors from it. This requires a permission prompt but works in more browsers. - Clear messaging — When neither approach fits, clearly inform the user that the feature requires a supported browser and provide guidance on how to upgrade.
async function openColorPicker() {
if ('EyeDropper' in window) {
// Preferred path: native eyedropper
const dropper = new EyeDropper();
const result = await dropper.open();
return result.sRGBHex;
} else {
// Fallback: use a color input dialog
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'color';
input.style.position = 'fixed';
input.style.top = '-100px';
input.style.left = '-100px';
document.body.appendChild(input);
input.addEventListener('change', () => {
resolve(input.value);
document.body.removeChild(input);
});
input.addEventListener('cancel', () => {
resolve(null);
document.body.removeChild(input);
});
// Trigger the native color picker
input.click();
});
}
}
This hybrid approach ensures your application works everywhere, delivering the best experience when the API is available and a reasonable alternative when it's not.
Handle the AbortError as a Normal Flow
When a user dismisses the eyedropper without selecting a color, the Promise rejects with an AbortError. This is not a bug or exceptional condition — it's an expected user action. Your code should treat it as a normal cancellation, without logging it as an error or showing an alert. Distinguish it from genuine errors like NotAllowedError or SecurityError.
try {
const result = await eyeDropper.open();
// Success path
applyColor(result.sRGBHex);
} catch (err) {
if (err.name === 'AbortError') {
// User chose not to pick a color — this is fine, do nothing
return;
}
// Actual error — log and potentially notify
console.error('EyeDropper failed:', err);
showToast('Could not open color sampler', 'error');
}
Consider Accessibility
The EyeDropper API relies on the user's ability to see and precisely target pixels on their screen. This presents inherent accessibility challenges for users with visual impairments or motor difficulties. Keep these considerations in mind:
- Provide an accessible text-based alternative for entering colors (hex input field, preset color palettes, or an
input[type=color]fallback). - Ensure the button that triggers the eyedropper has a clear, descriptive label or aria-label.
- Announce the selected color to screen readers when the eyedropper returns a result, using a live region or an aria-live element.
- Don't make the eyedropper the only way to input colors — always offer an alternative method.
<button id="pickBtn" aria-label="Pick a color from anywhere on your screen using the eyedropper tool">
🎨 Sample Color
</button>
<div aria-live="polite" aria-atomic="true" class="sr-announcement"></div>
<script>
const announcement = document.querySelector('.sr-announcement');
// After successful sampling
announcement.textContent = `Color selected: ${result.sRGBHex}`;
</script>
Reuse EyeDropper Instances
While the EyeDropper constructor is lightweight, it's good practice to create a single instance and reuse it across multiple sampling sessions within the same logical context. There's no state carried between open() calls, so reusing an instance is safe and avoids unnecessary object allocations:
// Create once
const sharedEyeDropper = new EyeDropper();
// Use multiple times
document.getElementById('btn1').addEventListener('click', async () => {
const result = await sharedEyeDropper.open();
console.log('First pick:', result.sRGBHex);
});
document.getElementById('btn2').addEventListener('click', async () => {
const result = await sharedEyeDropper.open();
console.log('Second pick:', result.sRGBHex);
});
Combine with Clipboard and Sharing APIs
The EyeDropper API works beautifully with other modern browser APIs. A common pattern is to combine it with the Async Clipboard API to let users sample a color and immediately copy it to the clipboard:
async function sampleAndCopy() {
const eyeDropper = new EyeDropper();
try {
const result = await eyeDropper.open();
await navigator.clipboard.writeText(result.sRGBHex);
console.log('Color copied to clipboard:', result.sRGBHex);
} catch (err) {
if (err.name === 'AbortError') return;
console.error('Operation failed:', err);
}
}
You can also integrate with the Web Share API to share a color palette, or with service workers to cache frequently used colors for offline access in a design application.
Conclusion
The EyeDropper API represents a significant leap forward for web-based creative tools, design applications, and developer utilities. It brings a capability that was previously exclusive to native applications into the browser platform, wrapped in a remarkably simple and well-designed interface. With just a few lines of code, you can give your users the ability to sample any color from their screen — no permissions drama, no heavy dependencies, no hacky workarounds.
While browser support is currently limited to Chromium-based browsers, the API's design aligns with the broader web platform trajectory of bringing native capabilities to the web in a secure, user-centric manner. Feature detection and thoughtful fallbacks ensure your application remains functional across all browsers today, while being ready to deliver the best experience where the API is available.
By following the patterns and best practices outlined in this guide — respecting user activation, handling errors gracefully, providing accessible alternatives, and combining the API with complementary web platform features — you can build polished, professional-grade color sampling experiences that delight your users and expand what your web application can do.