Claude
Skills
Sign in
Back

dark-mode-tester

Included with Lifetime
$97 forever

Test light and dark color scheme rendering by emulating prefers-color-scheme via CDP, capturing screenshots in both modes, computing WCAG 2.1 contrast ratios for all visible text, detecting hardcoded colors that ignore scheme changes, and validating color-scheme meta/CSS declarations.

Web Dev

What this skill does


# Dark Mode Tester

Perform a thorough dark mode audit by toggling `prefers-color-scheme` between
light and dark via Chrome DevTools Protocol, then comparing rendered colors,
contrast ratios, and visual output across both modes.

## When to Use

- Verifying that dark mode is implemented correctly across all components.
- Checking WCAG 2.1 contrast compliance in both light and dark modes.
- Finding elements with hardcoded colors that do not respond to scheme changes.
- Auditing `color-scheme` meta tag and CSS property declarations.
- Comparing visual differences between light and dark screenshots.

## Prerequisites

- **Playwright MCP server** connected and responding (all `mcp__playwright__browser_*` tools available).
- **Chromium-based browser** required for CDP `Emulation.setEmulatedMedia`.
- Target page must support `prefers-color-scheme` media query (or the audit will reveal that it does not).

## Workflow

### Step 1 -- Navigate to the Target Page

```
browser_navigate({ url: "<target_url>" })
```

### Step 2 -- Validate color-scheme Declarations

Check for `<meta name="color-scheme">` and CSS `color-scheme` property on the
root element.

```javascript
browser_evaluate({
  function: `() => {
    const results = { meta: null, cssRoot: null, cssBody: null };

    // Check meta tag
    const meta = document.querySelector('meta[name="color-scheme"]');
    results.meta = meta ? meta.content : 'NOT FOUND';

    // Check CSS color-scheme on :root and body
    const root = document.documentElement;
    const body = document.body;
    results.cssRoot = getComputedStyle(root).colorScheme || 'NOT SET';
    results.cssBody = getComputedStyle(body).colorScheme || 'NOT SET';

    return results;
  }`
})
```

### Step 3 -- Emulate Light Mode and Capture Baseline

Use CDP to force light mode, then collect all text element colors.

```javascript
browser_run_code({
  code: `async (page) => {
    const client = await page.context().newCDPSession(page);
    await client.send('Emulation.setEmulatedMedia', {
      features: [{ name: 'prefers-color-scheme', value: 'light' }]
    });
    // Allow re-render
    await page.waitForTimeout(1000);
    return 'Light mode emulated';
  }`
})
```

```
browser_take_screenshot({ type: "png", filename: "dark-mode-light.png" })
```

```
browser_snapshot()
```

Collect computed colors for all visible text elements in light mode:

```javascript
browser_evaluate({
  function: `() => {
    const elements = [];
    const selector = 'h1,h2,h3,h4,h5,h6,p,span,a,li,td,th,label,button,input,textarea,select,div,section,article';
    document.querySelectorAll(selector).forEach(el => {
      const style = getComputedStyle(el);
      const rect = el.getBoundingClientRect();
      // Skip invisible elements
      if (rect.width === 0 || rect.height === 0 || style.display === 'none' || style.visibility === 'hidden') return;
      // Only include elements with direct text content
      const hasDirectText = Array.from(el.childNodes).some(n => n.nodeType === 3 && n.textContent.trim());
      if (!hasDirectText && !['INPUT','TEXTAREA','SELECT','BUTTON'].includes(el.tagName)) return;

      elements.push({
        tag: el.tagName,
        id: el.id || null,
        class: el.className ? String(el.className).split(' ')[0] : null,
        color: style.color,
        backgroundColor: style.backgroundColor,
        text: el.textContent.trim().substring(0, 60)
      });
    });
    window.__lightColors = elements;
    return { totalElements: elements.length, sample: elements.slice(0, 10) };
  }`
})
```

### Step 4 -- Emulate Dark Mode and Capture

Switch to dark mode and collect the same data.

```javascript
browser_run_code({
  code: `async (page) => {
    const client = await page.context().newCDPSession(page);
    await client.send('Emulation.setEmulatedMedia', {
      features: [{ name: 'prefers-color-scheme', value: 'dark' }]
    });
    await page.waitForTimeout(1000);
    return 'Dark mode emulated';
  }`
})
```

```
browser_take_screenshot({ type: "png", filename: "dark-mode-dark.png" })
```

```
browser_snapshot()
```

Collect computed colors in dark mode:

```javascript
browser_evaluate({
  function: `() => {
    const elements = [];
    const selector = 'h1,h2,h3,h4,h5,h6,p,span,a,li,td,th,label,button,input,textarea,select,div,section,article';
    document.querySelectorAll(selector).forEach(el => {
      const style = getComputedStyle(el);
      const rect = el.getBoundingClientRect();
      if (rect.width === 0 || rect.height === 0 || style.display === 'none' || style.visibility === 'hidden') return;
      const hasDirectText = Array.from(el.childNodes).some(n => n.nodeType === 3 && n.textContent.trim());
      if (!hasDirectText && !['INPUT','TEXTAREA','SELECT','BUTTON'].includes(el.tagName)) return;

      elements.push({
        tag: el.tagName,
        id: el.id || null,
        class: el.className ? String(el.className).split(' ')[0] : null,
        color: style.color,
        backgroundColor: style.backgroundColor,
        text: el.textContent.trim().substring(0, 60)
      });
    });
    window.__darkColors = elements;
    return { totalElements: elements.length, sample: elements.slice(0, 10) };
  }`
})
```

### Step 5 -- Compute Contrast Ratios (WCAG 2.1)

Calculate contrast ratios for all text elements in both modes using the
relative luminance formula from WCAG 2.1.

```javascript
browser_evaluate({
  function: `() => {
    function parseColor(str) {
      const m = str.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);
      if (!m) return null;
      return { r: parseInt(m[1]), g: parseInt(m[2]), b: parseInt(m[3]) };
    }

    function sRGBtoLinear(c) {
      c = c / 255;
      return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
    }

    function luminance(rgb) {
      return 0.2126 * sRGBtoLinear(rgb.r) + 0.7152 * sRGBtoLinear(rgb.g) + 0.0722 * sRGBtoLinear(rgb.b);
    }

    function contrastRatio(fg, bg) {
      const l1 = Math.max(luminance(fg), luminance(bg));
      const l2 = Math.min(luminance(fg), luminance(bg));
      return (l1 + 0.05) / (l2 + 0.05);
    }

    function gradeContrast(ratio, isLargeText) {
      if (isLargeText) {
        if (ratio >= 4.5) return 'AAA';
        if (ratio >= 3) return 'AA';
        return 'FAIL';
      }
      if (ratio >= 7) return 'AAA';
      if (ratio >= 4.5) return 'AA';
      return 'FAIL';
    }

    function analyzeMode(elements, mode) {
      const results = [];
      for (const el of elements) {
        const fg = parseColor(el.color);
        const bg = parseColor(el.backgroundColor);
        if (!fg || !bg) continue;
        const ratio = Math.round(contrastRatio(fg, bg) * 100) / 100;
        const isLarge = ['H1','H2','H3'].includes(el.tag);
        const grade = gradeContrast(ratio, isLarge);
        if (grade === 'FAIL') {
          results.push({
            element: el.tag + (el.id ? '#' + el.id : '') + (el.class ? '.' + el.class : ''),
            text: el.text,
            foreground: el.color,
            background: el.backgroundColor,
            ratio: ratio,
            grade: grade,
            mode: mode
          });
        }
      }
      return results;
    }

    const lightFails = analyzeMode(window.__lightColors || [], 'light');
    const darkFails = analyzeMode(window.__darkColors || [], 'dark');

    return {
      lightMode: { total: (window.__lightColors || []).length, failures: lightFails.length, details: lightFails.slice(0, 20) },
      darkMode: { total: (window.__darkColors || []).length, failures: darkFails.length, details: darkFails.slice(0, 20) }
    };
  }`
})
```

### Step 6 -- Detect Hardcoded Colors (Unchanged Between Modes)

Find elements whose foreground or background color did not change between light
and dark mode, indicating hardcoded values that ignore the color scheme.

```javascript
browser_evaluate({
  function: `() => {
    const light = window.__lightColors || [];
    const dark = window.__darkColors || [];
   

Related in Web Dev