← Back to DevBytes

EyeDropper API: Complete Guide

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:

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:

The ColorSelectionResult object contains a single property:

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:

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:

<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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles