Introduction to the EyeDropper API
The EyeDropper API is a modern browser capability that allows web applications to sample colors from anywhere on the user's screen — including areas outside the browser window. This opens up creative possibilities for color picking tools, design applications, and accessibility features that were previously only possible with native desktop software or cumbersome workarounds.
What Is the EyeDropper API?
The EyeDropper API provides a simple, secure interface that lets users pick a single color value from any pixel displayed on their screen. When invoked, the browser presents a magnified eyedropper cursor. The user moves it over any visual element — whether inside the current webpage, in another browser tab, or even on a completely different application — and clicks to select a color. The API then resolves with the hex color code of that pixel.
At its core, the API is exposed through the EyeDropper interface on the window object. It is intentionally minimal: you create an instance, call its open() method, and receive a promise that resolves to an object containing the sRGBHex string — a six-character hexadecimal color code prefixed with #.
Why It Matters for Modern Web Applications
Before the EyeDropper API, web-based color sampling required indirect approaches. Developers had to instruct users to screenshot their screen, paste it into a canvas element, and then extract pixel data — a clunky multi-step process. Alternatively, applications relied on proprietary browser extensions or native platform bridges. The EyeDropper API eliminates these hurdles by providing a standardized, cross-browser mechanism that works with a single user gesture.
This matters for several categories of applications:
- Design and prototyping tools: Figma-style web apps can let users sample colors from reference images, brand guidelines, or competitor websites without leaving the browser.
- Accessibility checkers: Tools that analyze color contrast can allow users to directly sample foreground and background colors from any rendered content.
- Theme generators: Applications that extract color palettes from images can let users point at anything on screen as inspiration.
- Developer tools: Browser-based dev tools can sample colors from live web pages for debugging CSS or SVG styling.
- Creative coding: Generative art applications can let users pick seed colors from their environment.
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. You should always perform feature detection before attempting to use the API to avoid runtime errors and provide graceful fallbacks.
Feature detection is straightforward — check if the EyeDropper constructor exists on the global scope:
<script>
// Feature detection for EyeDropper API
if ('EyeDropper' in window) {
console.log('EyeDropper API is available!');
// Proceed with eyedropper functionality
} else {
console.warn('EyeDropper API not supported in this browser.');
// Provide fallback UI or instructions
}
</script>
You can also use a more defensive check that verifies the constructor is callable:
<script>
function isEyeDropperSupported() {
try {
return typeof window.EyeDropper === 'function'
&& 'open' in window.EyeDropper.prototype;
} catch (e) {
return false;
}
}
if (isEyeDropperSupported()) {
// Enable eyedropper button
document.getElementById('pick-color-btn').disabled = false;
} else {
document.getElementById('pick-color-btn').disabled = true;
document.getElementById('fallback-message').hidden = false;
}
</script>
Basic Usage: Opening the EyeDropper
The API is designed around a transient user activation model. The open() method must be called within a short time window following a user gesture — typically a click or keypress. This prevents websites from opening the eyedropper without the user's explicit intent, which would be a privacy and security concern.
Here is the minimal pattern for opening the eyedropper and obtaining a color:
<button id="activate-eyedropper">Pick a color from your screen</button>
<div id="color-result">Selected color will appear here</div>
<script>
const pickButton = document.getElementById('activate-eyedropper');
const resultDiv = document.getElementById('color-result');
pickButton.addEventListener('click', async () => {
// Create a new EyeDropper instance
const eyeDropper = new EyeDropper();
try {
// Open the eyedropper — this triggers the browser UI
const result = await eyeDropper.open();
// result.sRGBHex contains the hex color string, e.g., "#FF5733"
const hexColor = result.sRGBHex;
// Display the result
resultDiv.textContent = `Selected: ${hexColor}`;
resultDiv.style.backgroundColor = hexColor;
resultDiv.style.color = getContrastTextColor(hexColor);
} catch (error) {
// User dismissed the eyedropper or an error occurred
if (error.name === 'AbortError') {
resultDiv.textContent = 'Color selection cancelled.';
} else {
console.error('EyeDropper error:', error);
resultDiv.textContent = 'An error occurred while picking a color.';
}
}
});
// Helper function to determine readable text color on a background
function getContrastTextColor(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.5 ? '#000000' : '#FFFFFF';
}
</script>
Notice that the open() call returns a promise. The promise resolves when the user clicks on a pixel, or rejects if the user presses Escape, clicks outside the browser viewport in a way that cancels the operation, or if the browser encounters an internal error. The rejection object for user cancellation is an AbortError, which you can distinguish by checking error.name.
Handling the Color Result
The resolved value is an object with a single property: sRGBHex. This is always a string in the format #RRGGBB — six hexadecimal digits representing the red, green, and blue channels in the sRGB color space. There are no alpha transparency values, no expanded color spaces like Display P3, and no alternative formats. The API deliberately keeps the output simple and predictable.
Once you have the hex string, you can use it in countless ways:
<script>
async function pickAndApplyColor() {
const eyeDropper = new EyeDropper();
try {
const { sRGBHex } = await eyeDropper.open();
// Convert hex to RGB components
const hexToRgb = (hex) => ({
r: parseInt(hex.slice(1, 3), 16),
g: parseInt(hex.slice(3, 5), 16),
b: parseInt(hex.slice(5, 7), 16)
});
const rgb = hexToRgb(sRGBHex);
// Convert to HSL for more intuitive manipulation
const rgbToHsl = ({ r, g, b }) => {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const l = (max + min) / 2;
if (max === min) return { h: 0, s: 0, l };
const d = max - min;
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
let h;
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)) * 60; break;
case g: h = ((b - r) / d + 2) * 60; break;
case b: h = ((r - g) / d + 4) * 60; break;
}
return { h: Math.round(h), s: Math.round(s * 100), l: Math.round(l * 100) };
};
const hsl = rgbToHsl(rgb);
console.log('Hex:', sRGBHex);
console.log('RGB:', rgb);
console.log('HSL:', hsl);
// Use these values to update UI elements, generate palettes, etc.
return { hex: sRGBHex, rgb, hsl };
} catch (err) {
if (err.name === 'AbortError') {
console.log('User cancelled color pick.');
return null;
}
throw err;
}
}
</script>
The sRGB hex format matches CSS color syntax exactly, so you can use it directly in any CSS property that accepts colors — background-color, color, border-color, box-shadow, SVG fill and stroke, canvas fill styles, and WebGL uniforms.
Building a Complete Color Picker Component
Let's build a reusable, self-contained color picker component that integrates the EyeDropper API with a manual color input and a visual preview. This demonstrates how to combine the eyedropper with traditional color picking methods for a full-featured experience.
<div class="color-picker-widget">
<div class="color-preview">
<div class="color-swatch" id="swatch"></div>
<span class="color-hex" id="hex-display">#000000</span>
</div>
<div class="color-controls">
<input type="color" id="manual-picker" value="#000000">
<button id="eyedropper-trigger" title="Pick color from screen">
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M13.5 2.5a2 2 0 0 0-2.8 0L9 4.2l-.7-.7a1 1 0 1 0-1.4 1.4l.7.7-6.6 6.6v2.8h2.8l6.6-6.6.7.7a1 1 0 1 0 1.4-1.4l-.7-.7 1.7-1.7a2 2 0 0 0 0-2.8z" fill="currentColor"/>
</svg>
Eyedropper
</button>
<button id="copy-hex">Copy Hex</button>
</div>
<div class="color-history" id="color-history">
<h4>Recent Colors</h4>
<div class="history-swatches" id="history-swatches"></div>
</div>
</div>
<style>
.color-picker-widget {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
max-width: 320px;
font-family: system-ui, sans-serif;
}
.color-swatch {
width: 100%;
height: 80px;
border-radius: 6px;
border: 2px solid #ccc;
transition: background-color 0.2s;
}
.color-hex {
display: block;
text-align: center;
font-size: 1.2em;
font-weight: bold;
margin-top: 8px;
font-family: monospace;
}
.color-controls {
display: flex;
gap: 8px;
margin-top: 12px;
align-items: center;
}
.color-controls button {
padding: 6px 12px;
border: 1px solid #999;
border-radius: 4px;
background: #f5f5f5;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
.color-controls button:hover {
background: #e8e8e8;
}
#manual-picker {
width: 40px;
height: 40px;
border: none;
cursor: pointer;
}
.history-swatches {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.history-swatch {
width: 28px;
height: 28px;
border-radius: 4px;
border: 1px solid #ccc;
cursor: pointer;
transition: transform 0.15s;
}
.history-swatch:hover {
transform: scale(1.2);
border-color: #666;
}
</style>
<script>
class ColorPickerComponent {
constructor(container) {
this.container = container;
this.swatch = container.querySelector('#swatch');
this.hexDisplay = container.querySelector('#hex-display');
this.manualPicker = container.querySelector('#manual-picker');
this.eyedropperTrigger = container.querySelector('#eyedropper-trigger');
this.copyButton = container.querySelector('#copy-hex');
this.historyContainer = container.querySelector('#history-swatches');
this.currentColor = '#000000';
this.colorHistory = this.loadHistory();
this.init();
}
init() {
this.updateUI(this.currentColor);
// Manual color input
this.manualPicker.addEventListener('input', (e) => {
this.setColor(e.target.value);
});
// EyeDropper trigger
if (this.isEyedropperSupported()) {
this.eyedropperTrigger.addEventListener('click', () => this.pickFromScreen());
} else {
this.eyedropperTrigger.disabled = true;
this.eyedropperTrigger.title = 'EyeDropper not supported in this browser';
}
// Copy button
this.copyButton.addEventListener('click', () => this.copyHex());
this.renderHistory();
}
isEyedropperSupported() {
return 'EyeDropper' in window;
}
setColor(hex) {
this.currentColor = hex;
this.updateUI(hex);
this.addToHistory(hex);
}
updateUI(hex) {
this.swatch.style.backgroundColor = hex;
this.hexDisplay.textContent = hex;
this.manualPicker.value = hex;
}
async pickFromScreen() {
const eyeDropper = new EyeDropper();
try {
const result = await eyeDropper.open();
this.setColor(result.sRGBHex);
} catch (error) {
if (error.name === 'AbortError') {
// User cancelled — do nothing, keep current color
console.log('EyeDropper: user cancelled selection.');
} else {
console.error('EyeDropper error:', error);
alert('Could not pick color. Please try again.');
}
}
}
addToHistory(hex) {
// Remove duplicate if exists
this.colorHistory = this.colorHistory.filter(c => c !== hex);
// Add to front
this.colorHistory.unshift(hex);
// Keep last 20 colors
this.colorHistory = this.colorHistory.slice(0, 20);
this.saveHistory();
this.renderHistory();
}
renderHistory() {
this.historyContainer.innerHTML = '';
this.colorHistory.forEach(hex => {
const swatch = document.createElement('div');
swatch.className = 'history-swatch';
swatch.style.backgroundColor = hex;
swatch.title = hex;
swatch.addEventListener('click', () => this.setColor(hex));
this.historyContainer.appendChild(swatch);
});
}
copyHex() {
navigator.clipboard.writeText(this.currentColor).then(() => {
const originalText = this.copyButton.textContent;
this.copyButton.textContent = 'Copied!';
setTimeout(() => {
this.copyButton.textContent = originalText;
}, 1500);
}).catch(err => {
console.error('Clipboard write failed:', err);
});
}
loadHistory() {
try {
const stored = localStorage.getItem('eyedropper-color-history');
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
}
saveHistory() {
try {
localStorage.setItem('eyedropper-color-history', JSON.stringify(this.colorHistory));
} catch {
// localStorage may be full or unavailable
}
}
}
// Initialize the component
document.addEventListener('DOMContentLoaded', () => {
const container = document.querySelector('.color-picker-widget');
if (container) {
new ColorPickerComponent(container);
}
});
</script>
This component demonstrates several important patterns: wrapping the EyeDropper API in a feature-detection guard, maintaining a fallback color input, persisting color history in localStorage, and providing clipboard integration. The component gracefully degrades when the EyeDropper API is unavailable — the eyedropper button simply becomes disabled with an explanatory tooltip.
Integrating with Canvas and Image Elements
A powerful pattern is combining the EyeDropper API with HTML canvas for scenarios where you want to let users sample colors from an uploaded image displayed within your application. While the eyedropper can pick colors from the image rendered in the browser, you might also want to identify which pixel in the source image corresponds to the picked color — for example, to extract a palette or match a specific region.
Here is an integration that loads an image onto a canvas and uses the eyedropper to sample colors from it, mapping the picked color back to image coordinates:
<canvas id="image-canvas" width="800" height="600"></canvas>
<input type="file" id="image-loader" accept="image/*">
<button id="sample-from-canvas">Sample color from canvas</button>
<div id="pixel-info">Click the eyedropper on the canvas to identify pixels</div>
<script>
const canvas = document.getElementById('image-canvas');
const ctx = canvas.getContext('2d');
const sampleBtn = document.getElementById('sample-from-canvas');
const pixelInfo = document.getElementById('pixel-info');
let imageData = null;
// Load an image file and draw it on the canvas
document.getElementById('image-loader').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const img = new Image();
img.onload = () => {
// Scale to fit canvas while maintaining aspect ratio
const scale = Math.min(canvas.width / img.width, canvas.height / img.height);
const w = img.width * scale;
const h = img.height * scale;
const x = (canvas.width - w) / 2;
const y = (canvas.height - h) / 2;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, x, y, w, h);
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
};
img.src = URL.createObjectURL(file);
});
sampleBtn.addEventListener('click', async () => {
if (!('EyeDropper' in window)) {
alert('EyeDropper API is not available in your browser.');
return;
}
const eyeDropper = new EyeDropper();
try {
const result = await eyeDropper.open();
const hex = result.sRGBHex;
// Convert hex to RGB for pixel matching
const targetR = parseInt(hex.slice(1, 3), 16);
const targetG = parseInt(hex.slice(3, 5), 16);
const targetB = parseInt(hex.slice(5, 7), 16);
// Search the image data for matching pixels
// This is a simple approach — in production you'd use tolerance matching
const matches = [];
const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
const idx = (y * canvas.width + x) * 4;
const r = data[idx];
const g = data[idx + 1];
const b = data[idx + 2];
// Exact match (you may want a tolerance of ±2 per channel)
if (r === targetR && g === targetG && b === targetB) {
matches.push({ x, y, r, g, b });
if (matches.length > 50) break; // Limit results
}
}
if (matches.length > 50) break;
}
if (matches.length > 0) {
pixelInfo.innerHTML = `
<strong>Sampled: ${hex}</strong><br>
Found ${matches.length} matching pixel(s) in the image.<br>
First match at (${matches[0].x}, ${matches[0].y})
`;
// Highlight the first matching pixel on the canvas
ctx.fillStyle = '#FF0000';
ctx.fillRect(matches[0].x - 2, matches[0].y - 2, 5, 5);
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(matches[0].x - 1, matches[0].y - 1, 3, 3);
} else {
pixelInfo.innerHTML = `
<strong>Sampled: ${hex}</strong><br>
No exact match found in the image. The color may be from outside the canvas.
`;
}
} catch (error) {
if (error.name === 'AbortError') {
pixelInfo.textContent = 'Sampling cancelled.';
}
}
});
</script>
This pattern is particularly useful in applications where users upload reference images and need to extract specific color values from them. The combination of canvas pixel manipulation and the EyeDropper API bridges the gap between visual sampling and programmatic color analysis.
Error Handling and Edge Cases
Robust error handling is essential when using the EyeDropper API. Several distinct error scenarios can occur, and your application should handle each gracefully.
The open() method can reject for these reasons:
- AbortError: The user dismissed the eyedropper UI by pressing Escape or clicking the cancel mechanism. This is not a true error — it's user intent. Your code should treat it as a cancellation, not a failure.
- NotAllowedError: The
open()call was made without a recent user gesture (transient activation). This can happen if you callopen()inside asetTimeoutor after an async operation without proper user interaction context. - InvalidStateError: The API is already active — you cannot have multiple concurrent eyedropper sessions.
- OperationError: A platform-specific failure, such as the operating system's color sampling mechanism being temporarily unavailable.
Here is a comprehensive error handling wrapper:
<script>
async function safeEyedropperOpen(options = {}) {
const {
timeout = 30000, // Max wait time in ms (30 seconds)
onCancelled = () => {},
onError = () => {}
} = options;
if (!('EyeDropper' in window)) {
const error = new Error('EyeDropper API not supported');
error.code = 'UNSUPPORTED';
onError(error);
throw error;
}
const eyeDropper = new EyeDropper();
// Create a timeout promise to avoid hanging indefinitely
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('EyeDropper operation timed out'));
}, timeout);
});
try {
// Race the eyedropper against the timeout
const result = await Promise.race([
eyeDropper.open(),
timeoutPromise
]);
// Validate the result
if (!result || typeof result.sRGBHex !== 'string') {
throw new Error('Invalid color result from EyeDropper');
}
// Validate hex format
const hexPattern = /^#[0-9A-Fa-f]{6}$/;
if (!hexPattern.test(result.sRGBHex)) {
throw new Error(`Unexpected color format: ${result.sRGBHex}`);
}
return result.sRGBHex;
} catch (error) {
if (error.name === 'AbortError') {
console.log('User cancelled eye dropper selection.');
onCancelled();
return null; // Return null for cancellation, don't throw
}
if (error.name === 'NotAllowedError') {
console.error('EyeDropper requires a user gesture. Ensure open() is called within an event handler.');
onError(error);
throw new Error('EyeDropper requires user interaction. Please click a button to activate.');
}
if (error.name === 'InvalidStateError') {
console.error('EyeDropper is already active. Close the previous instance first.');
onError(error);
throw new Error('An eye dropper session is already in progress.');
}
// Generic fallback for other errors
console.error('EyeDropper error:', error);
onError(error);
throw error;
}
}
// Usage example
document.getElementById('pick-btn').addEventListener('click', async () => {
try {
const color = await safeEyedropperOpen({
timeout: 15000,
onCancelled: () => {
document.getElementById('status').textContent = 'Selection cancelled.';
},
onError: (err) => {
document.getElementById('status').textContent = `Error: ${err.message}`;
}
});
if (color) {
console.log('Successfully picked:', color);
document.getElementById('status').textContent = `Picked: ${color}`;
}
} catch (err) {
// Only non-cancellation errors reach here
alert(err.message);
}
});
</script>
Notice the timeout wrapper. The EyeDropper API itself does not have a built-in timeout mechanism — if the user walks away from their computer while the eyedropper is active, the promise remains pending indefinitely. Wrapping it with Promise.race and a timeout ensures your application doesn't get stuck in a waiting state forever.
Best Practices for User Experience
Implementing the EyeDropper API well is as much about UX as it is about code. Here are key best practices drawn from real-world implementations:
1. Provide clear visual cues before activation. Users unfamiliar with the feature need to understand what will happen when they click your eyedropper button. Use tooltips, brief instructional text, or a small animation showing a magnifying glass or eyedropper icon. The button should have a recognizable icon — the standard eyedropper or color picker icon is widely understood.
<button id="eyedropper-btn"
title="Pick any color from your screen — including outside this window">
🎨 Pick Screen Color
</button>
2. Show immediate feedback after selection. The moment the user picks a color, the eyedropper UI closes. Your application should immediately reflect the selected color — update a swatch, fill a text field, or highlight a palette entry. A delay between selection and visible feedback feels broken.
3. Support undo and color history. Users often sample multiple colors before deciding. Maintain a history stack (as shown in the component example above) so users can revisit previously picked colors without re-sampling.
4. Handle the cancellation path gracefully. When a user presses Escape, don't show an error message. Simply leave the previous state intact. If there was no previous selection, maintain a neutral state.
5. Combine with manual input for precision. The eyedropper is great for sampling, but users also need to type exact hex values or adjust colors with sliders. Always pair the eyedropper with a standard <input type="color"> or text input for hex codes.
6. Respect transient activation requirements. Always call open() directly within a click event handler. Avoid wrapping it in unnecessary async operations that might consume the user gesture token. If you need to do pre-processing before opening the eyedropper, restructure your code so the user gesture triggers the eyedropper directly.
<script>
// GOOD: Direct call within event handler
button.addEventListener('click', async () => {
const eyeDropper = new EyeDropper();
const result = await eyeDropper.open();
// Process result...
});
// RISKY: The gesture token may expire during the delay
button.addEventListener('click', async () => {
await fetch('/some-preflight-check'); // This may consume the gesture
const eyeDropper = new EyeDropper();
const result = await eyeDropper.open(); // May throw NotAllowedError
});
</script>
7. Provide a fallback for unsupported browsers. Always check for API availability and provide an alternative. A good fallback is a standard color input with instructions to use a screenshot or external tool for sampling.
Accessibility Considerations
The EyeDropper API introduces unique accessibility challenges. When the eyedropper is active, the browser displays a magnified view around the cursor, which helps users with low vision target specific pixels. However, the API relies entirely on visual cursor positioning, which presents barriers for users who navigate with keyboards or assistive technologies.
Key accessibility considerations:
- Keyboard-only users: The eyedropper requires mouse or touch input to position the cursor. Provide an equivalent text-based hex input as a fully accessible alternative. The color picker component shown earlier does exactly this with the
<input type="color">and manual hex entry. - Screen reader announcements: Use
aria-liveregions to announce color selection results. When a color is picked, screen readers should receive an update. - Focus management: After the eyedropper closes, focus should return to the element that triggered it. Test this behavior across browsers, as focus handling after eyedropper dismissal can vary.
- Contrast requirements: The swatch displaying the picked color must meet WCAG contrast minimums against its background. If the picked color is very light, ensure the swatch has a visible border.
<div aria-live="polite" aria-atomic="true" id="color-announcement"></div>
<script>
async function pickWithAnnouncement() {
const eyeDropper = new EyeDropper();
try {
const result = await eyeDropper.open();
const hex = result.sRGBHex;
// Update the live region for screen readers
const announcement = document.getElementById('color-announcement');
announcement.textContent = `Color selected: ${hex}.
Red ${hex.slice(1,3)}, Green ${hex.slice(3,5)}, Blue ${hex.slice(5,7)}.`;
// Also update visual swatch
updateSwatch(hex);
// Return focus to the trigger button
document.getElementById('eyedropper-trigger').focus();
} catch (error) {
if (error.name === 'AbortError') {
document.getElementById('color-announcement').textContent =
'Color selection cancelled.';
}
}
}
</script>
Combining with Other APIs
The EyeDropper API becomes even more powerful when combined with other modern browser APIs. Here are some practical integrations:
Clipboard API — Copy picked colors:
<script>
async function pickAndCopy() {
const eyeDropper = new EyeDropper();
try {
const result = await eyeDropper.open();
await navigator.clipboard.writeText(result.sRGBHex);
console.log(`Copied ${result.sRGBHex} to clipboard!`);
} catch (err) {
if (err.name === 'AbortError') return;
console.error(err);
}
}
</script>
Web Share API — Share colors with other apps:
<script>
async function pickAndShare() {
const eyeDropper = new EyeDropper();
try {
const result = await