Skip to content

Conversation

@joaogsribeiro
Copy link

@joaogsribeiro joaogsribeiro commented Dec 6, 2025

Description

This PR adds a new utility: WCAG Contrast Checker.

It helps developers and designers ensure their color combinations meet accessibility standards (AA/AAA) based on official W3C formulas.

This project evolved from a simple color picker to a full accessibility tool, and finally to a Browser Extension (Manifest V3) with EyeDropper API support.

Linked Issue

Closes #443

Features

  • Real-time contrast ratio calculation (W3C relative luminance).
  • Dynamic preview box.
  • EyeDropper API integration (Pick color from screen).
  • Manual input support (HEX/RGB).
  • Logic kept under 100 lines of JS (96 lines).
  • Chrome Extension support (Manifest V3).

Tech Stack

  • HTML5, CSS3 (Flexbox/Grid), Vanilla JavaScript.

Thank you!

Copilot AI review requested due to automatic review settings December 6, 2025 00:58
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new WCAG Contrast Checker utility as a Chrome Extension (Manifest V3) to help developers and designers verify color combinations meet WCAG accessibility standards (AA/AAA). The tool implements real-time contrast ratio calculations using W3C formulas and provides multiple color selection methods including an EyeDropper API integration.

Key Changes:

  • Real-time WCAG contrast ratio calculation with AA (4.5:1) and AAA (7:1) compliance checking
  • Triple color input methods: color pickers, EyeDropper API for screen picking, and manual HEX/RGB input
  • Live preview and clipboard copy functionality for all color values

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
wcag-checker/index.html Main popup interface with semantic HTML structure for color selection, preview, and results display
wcag-checker/style.css Comprehensive styling with flexbox layout optimized for 400px popup width
wcag-checker/script.js Core JavaScript logic implementing W3C luminance calculations and user interactions
wcag-checker/manifest.json Chrome Extension Manifest V3 configuration with permissions declarations
wcag-checker/README.md Bilingual (English/Portuguese) documentation covering installation, features, and usage

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 13 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const result = await new EyeDropper().open();
target.value = result.sRGBHex.toUpperCase();
handleColorChange();
} catch (e) { console.log('Color selection cancelled'); }
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling in the EyeDropper pickColor function logs "Color selection cancelled" as a normal log message, but this might be confusing since it's in a catch block. User cancellation is expected behavior and shouldn't be treated as an error. Consider either removing the console.log entirely or making it more specific: if (e.name !== 'AbortError') console.error('EyeDropper error:', e);

Suggested change
} catch (e) { console.log('Color selection cancelled'); }
} catch (e) { if (e.name !== 'AbortError') console.error('EyeDropper error:', e); }

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +105
<span id="wcag-aa-status">PASS</span>
<label>WCAG AA</label>
</div>
<div class="status-group">
<span id="wcag-aaa-status">PASS</span>
<label>WCAG AAA</label>
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The status labels and values are positioned in reverse reading order. Currently showing status value first, then label. Consider swapping the order in the HTML or using flexbox flex-direction: row-reverse to maintain the current visual layout while improving the DOM order for screen readers.

Suggested change
<span id="wcag-aa-status">PASS</span>
<label>WCAG AA</label>
</div>
<div class="status-group">
<span id="wcag-aaa-status">PASS</span>
<label>WCAG AAA</label>
<label>WCAG AA</label>
<span id="wcag-aa-status">PASS</span>
</div>
<div class="status-group">
<label>WCAG AAA</label>
<span id="wcag-aaa-status">PASS</span>

Copilot uses AI. Check for mistakes.

## Permissões

- `activeTab`: Necessária para a funcionalidade do conta-gotas pegar cores da página atual
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Portuguese translation also incorrectly states that activeTab is necessary for the eyedropper functionality ("Necessária para a funcionalidade do conta-gotas pegar cores da página atual"). This is the same inaccuracy as in the English section - the EyeDropper API doesn't require this permission.

Suggested change
- `activeTab`: Necessária para a funcionalidade do conta-gotas pegar cores da página atual
- `activeTab`: Permite que a extensão acesse a aba ativa quando necessário

Copilot uses AI. Check for mistakes.
<div class="selector-group">
<label for="text-color-input">Text Color</label>
<div class="color-input-wrapper">
<input type="color" id="text-color-input" value="#000000" />
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing ARIA labels for the color picker inputs. Screen reader users won't get sufficient context when interacting with these color inputs. Consider adding aria-label attributes to the color input elements to describe their purpose (e.g., aria-label="Select text color").

Suggested change
<input type="color" id="text-color-input" value="#000000" />
<input type="color" id="text-color-input" value="#000000" aria-label="Select text color" />

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +44
function getLuminance(r, g, b) {
const a = [r, g, b].map((v) => {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getLuminance function uses magic numbers (0.03928, 12.92, 0.055, 1.055, 2.4, 0.2126, 0.7152, 0.0722) without explanation. While these are standard WCAG formula constants, adding a brief comment referencing the W3C specification would improve maintainability and help future developers understand why these specific values are used.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +104
<div id="contrast-ratio-value">21.00 : 1</div>
<div class="status-group">
<span id="wcag-aa-status">PASS</span>
<label>WCAG AA</label>
</div>
<div class="status-group">
<span id="wcag-aaa-status">PASS</span>
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial page load doesn't call handleColorChange(), so the contrast ratio and WCAG status are hardcoded in the HTML (line 98 shows "21.00 : 1" and lines 100, 104 show "PASS"). If JavaScript fails to load or execute, users will see incorrect information. Consider either calling handleColorChange() on page load or setting more neutral initial values in the HTML.

Suggested change
<div id="contrast-ratio-value">21.00 : 1</div>
<div class="status-group">
<span id="wcag-aa-status">PASS</span>
<label>WCAG AA</label>
</div>
<div class="status-group">
<span id="wcag-aaa-status">PASS</span>
<div id="contrast-ratio-value">--</div>
<div class="status-group">
<span id="wcag-aa-status">Unknown</span>
<label>WCAG AA</label>
</div>
<div class="status-group">
<span id="wcag-aaa-status">Unknown</span>

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +92
function handleManualInput(input, colorInput) {
const v = input.value.trim().toUpperCase();
if (v.startsWith("#") && /^#[0-9A-F]{6}$/i.test(v)) { colorInput.value = v; handleColorChange(); }
else if (v.startsWith("RGB")) {
const m = v.match(/RGB\((\d+),\s*(\d+),\s*(\d+)\)/);
if (m) { const [, r, g, b] = m.map(Number); if (r <= 255 && g <= 255 && b <= 255) { colorInput.value = rgbToHex(r, g, b); handleColorChange(); } }
}
}
[textHexValue, textRgbValue].forEach(el => el.addEventListener('blur', () => handleManualInput(el, textColorInput)));
[bgHexValue, bgRgbValue].forEach(el => el.addEventListener('blur', () => handleManualInput(el, bgColorInput)));
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual input handler uses different event types for text and background inputs but both are processed the same way. Lines 91-92 attach 'blur' events to all four input fields, but the logic doesn't differentiate between hex and RGB inputs for the same color. If a user types in the RGB field and then the hex field, both will trigger, potentially causing unexpected behavior or redundant updates.

Suggested change
function handleManualInput(input, colorInput) {
const v = input.value.trim().toUpperCase();
if (v.startsWith("#") && /^#[0-9A-F]{6}$/i.test(v)) { colorInput.value = v; handleColorChange(); }
else if (v.startsWith("RGB")) {
const m = v.match(/RGB\((\d+),\s*(\d+),\s*(\d+)\)/);
if (m) { const [, r, g, b] = m.map(Number); if (r <= 255 && g <= 255 && b <= 255) { colorInput.value = rgbToHex(r, g, b); handleColorChange(); } }
}
}
[textHexValue, textRgbValue].forEach(el => el.addEventListener('blur', () => handleManualInput(el, textColorInput)));
[bgHexValue, bgRgbValue].forEach(el => el.addEventListener('blur', () => handleManualInput(el, bgColorInput)));
function handleManualInput(input, colorInput, siblingField) {
const v = input.value.trim().toUpperCase();
if (v.startsWith("#") && /^#[0-9A-F]{6}$/i.test(v)) {
colorInput.value = v;
// Update sibling RGB field
if (siblingField) {
const r = parseInt(v.slice(1, 3), 16);
const g = parseInt(v.slice(3, 5), 16);
const b = parseInt(v.slice(5, 7), 16);
siblingField.value = `RGB(${r}, ${g}, ${b})`;
}
handleColorChange();
}
else if (v.startsWith("RGB")) {
const m = v.match(/RGB\((\d+),\s*(\d+),\s*(\d+)\)/);
if (m) {
const [, r, g, b] = m.map(Number);
if (r <= 255 && g <= 255 && b <= 255) {
const hex = rgbToHex(r, g, b).toUpperCase();
colorInput.value = hex;
// Update sibling hex field
if (siblingField) {
siblingField.value = hex;
}
handleColorChange();
}
}
}
}
[textHexValue, textRgbValue].forEach((el, idx, arr) => el.addEventListener('blur', () => handleManualInput(el, textColorInput, arr[1-idx])));
[bgHexValue, bgRgbValue].forEach((el, idx, arr) => el.addEventListener('blur', () => handleManualInput(el, bgColorInput, arr[1-idx])));

Copilot uses AI. Check for mistakes.

// Manual input handlers
function rgbToHex(r, g, b) { return "#" + [r, g, b].map(x => { const h = x.toString(16); return h.length === 1 ? "0" + h : h; }).join(""); }
function handleManualInput(input, colorInput) {
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The function name handleManualInput doesn't clearly indicate that it both validates AND updates the color input. A more descriptive name would be validateAndUpdateColorInput or parseManualColorInput to better convey what the function does.

Copilot uses AI. Check for mistakes.
try {
await navigator.clipboard.writeText(target.value);
btn.innerHTML = iconCheck; btn.disabled = true;
setTimeout(() => { btn.innerHTML = iconCopy; btn.disabled = false; }, 2000);
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copy button functionality disables the button for 2 seconds after copying, but doesn't handle rapid clicks. If a user clicks multiple times quickly, multiple timeouts will be created, potentially causing the button state to be inconsistent. Consider adding a guard to prevent multiple simultaneous copy operations.

Suggested change
setTimeout(() => { btn.innerHTML = iconCopy; btn.disabled = false; }, 2000);
if (btn._copyTimeout) clearTimeout(btn._copyTimeout);
btn._copyTimeout = setTimeout(() => {
btn.innerHTML = iconCopy; btn.disabled = false;
btn._copyTimeout = null;
}, 2000);

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +36
function hexToRgb(hex) {
const h = hex.replace("#", "");
return { r: parseInt(h.substring(0, 2), 16), g: parseInt(h.substring(2, 4), 16), b: parseInt(h.substring(4, 6), 16) };
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hexToRgb function doesn't validate the input hex string. If a malformed hex value is passed (e.g., from manual input before validation), parseInt could return NaN, leading to incorrect calculations in getLuminance. Consider adding validation or handling invalid hex values gracefully.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Color Picker Utility

1 participant