-
Notifications
You must be signed in to change notification settings - Fork 333
Add WCAG Contrast Checker Utility #452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add WCAG Contrast Checker Utility #452
Conversation
There was a problem hiding this 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.
There was a problem hiding this 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'); } |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
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);
| } catch (e) { console.log('Color selection cancelled'); } | |
| } catch (e) { if (e.name !== 'AbortError') console.error('EyeDropper error:', e); } |
| <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> |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
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.
| <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> |
|
|
||
| ## Permissões | ||
|
|
||
| - `activeTab`: Necessária para a funcionalidade do conta-gotas pegar cores da página atual |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
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.
| - `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 |
| <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" /> |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
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").
| <input type="color" id="text-color-input" value="#000000" /> | |
| <input type="color" id="text-color-input" value="#000000" aria-label="Select text color" /> |
| 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; |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
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.
| <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> |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
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.
| <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> |
| 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))); |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
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.
| 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]))); |
|
|
||
| // 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) { |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
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.
| try { | ||
| await navigator.clipboard.writeText(target.value); | ||
| btn.innerHTML = iconCheck; btn.disabled = true; | ||
| setTimeout(() => { btn.innerHTML = iconCopy; btn.disabled = false; }, 2000); |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
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.
| 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); |
| 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) }; |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
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.
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
Tech Stack
Thank you!