Files
cremote/GRADIENT_CONTRAST_FIX.md
Josh at WLTechBlog 87e5e0555d bump
2025-12-09 14:58:00 -07:00

7.1 KiB

Contrast Detection Fixes

Problems

The contrast checking tool had three issues that caused false positives and incorrect reporting:

Issue 1: White-on-White False Positives

Example: Vision Leadership sponsor page (https://visionleadership.org/sponsor-opportunities/)

  • Reported: White text rgb(255, 255, 255) on white background rgb(255, 255, 255) = 1:1 ratio (FAIL)
  • Reality: White text on gradient background linear-gradient(rgb(12, 90, 201) 0%, rgb(0, 0, 0) 100%)
  • Actual Contrast: 6.32:1 to 21:1 (PASS)

Root Cause: The gradient was on a parent container (.et_pb_section), not directly on the text elements. The tool only checked the element itself for gradients, not parent elements.

Issue 2: Gradient Applied to Wrong Elements

After the initial fix, the tool became too aggressive:

  • Reported: 54 elements with blue background rgb(12, 90, 201) (from gradient)
  • Reality: Those elements were on light gray rgb(252, 252, 252) background
  • Root Cause: Gradient detection walked up the tree but didn't stop when it encountered solid backgrounds

Issue 3: Wrong Text Color for Elements with Child Spans

The tool used parent element colors instead of actual visible text colors:

  • Example: <a style="color: rgb(12, 113, 195)"><span style="color: #ffffff;">314-560-7171</span><br></a>
  • Reported: Used the <a>'s color (blue rgb(12, 113, 195)) = 4.17:1 contrast
  • Reality: The visible text uses the <span>'s color (white rgb(255, 255, 255)) = 21.00:1 contrast
  • Root Cause: Tool only checked the matched element's color, not descendant elements that contain the actual text

Solutions

Modified the contrast detection logic in daemon/daemon.go to:

  1. Walk up the DOM tree to find gradient backgrounds on parent elements
  2. Stop at solid backgrounds - if a solid background color is found before a gradient, use the solid color
  3. Only apply gradients when no solid background exists between the element and the gradient
  4. Check child elements for text color - if an element has visible child elements with text, use the first child's color instead of the parent's color

Code Changes

1. Enhanced analyzeGradientContrast Function (Lines 9152-9220)

Before: Only checked the element itself for gradients

After:

  • Walks up the DOM tree looking for gradients
  • Checks for solid backgrounds at each level
  • Returns null if a solid background is found (gradient doesn't apply)
  • Only returns gradient info if no solid background blocks it
function analyzeGradientContrast(element, textColor) {
    let current = element;
    while (current && current !== document.documentElement.parentElement) {
        const style = window.getComputedStyle(current);
        
        // Check for solid background first - if found, gradient doesn't apply
        const bgColor = style.backgroundColor;
        if (!isTransparent(bgColor)) {
            return null; // Solid background found
        }

        // Check for gradient
        const bgImage = style.backgroundImage;
        if (bgImage && bgImage.includes('gradient')) {
            // Extract and analyze gradient colors...
            return gradientInfo;
        }

        current = current.parentElement;
    }
    return null;
}

2. Updated Background Detection Logic (Lines 9305-9340)

Before: Always checked for gradients, even when solid backgrounds were found

After:

  • Tier 1: Check for solid backgrounds via DOM tree walking
  • Tier 2: Check for solid backgrounds via stacking context
  • Tier 3: Only check for gradients if no solid background found
  • Fallback: Use white if nothing found
// Tier 1: DOM tree walking for solid colors
bgColor = getEffectiveBackgroundDOMTree(element);

// Tier 2: Stacking context analysis (if Tier 1 failed)
if (!bgColor) {
    bgColor = getBackgroundFromStackingContext(element);
}

// Tier 3: Check for gradients (only if no solid color found)
if (!bgColor) {
    gradientInfo = analyzeGradientContrast(element, fgColor);
    if (gradientInfo) {
        bgColor = gradientInfo.colors[0];
    }
}

// Fallback to white
if (!bgColor) {
    bgColor = 'rgb(255, 255, 255)';
}

Testing

After deploying the fix, test on:

  1. Vision Leadership sponsor page - Should correctly detect gradient and report passing contrast
  2. Elements with solid backgrounds - Should not incorrectly apply gradients from parent containers
  3. Nested backgrounds - Should use the closest background (solid or gradient)

Expected Results

Vision Leadership Hero Section

  • H1 "Sponsorship Opportunities That Work For You"
    • Detection method: gradient-analysis
    • Gradient colors: rgb(12, 90, 201) to rgb(0, 0, 0)
    • Worst contrast: 6.32:1 (PASS AA for large text)
    • Best contrast: 21:1 (PASS AAA)

Elements with Solid Backgrounds

  • Text on light gray rgb(252, 252, 252)
    • Detection method: dom-tree or stacking-context
    • Should NOT use gradient from parent
    • Should calculate contrast against actual gray background

Deployment

  1. Build the daemon: make daemon
  2. Build the MCP server: make mcp
  3. Restart the cremote daemon (deployment-specific process)
  4. Restart the MCP server or Augment extension to pick up changes

3. Enhanced Text Color Detection (Lines 9301-9354)

Before: Always used the matched element's text color

After:

  • Recursively searches all descendant elements to find the one with the most text content
  • Uses that element's color for contrast checking
  • Handles complex structures like <a><span>text</span><br></a>
  • Skips invisible elements (display: none, visibility: hidden)
// Recursively find the element with the most text content
function findTextBearingElement(el) {
    const descendants = el.querySelectorAll('*');
    let bestElement = el;
    let maxTextLength = 0;

    // Check the element itself first
    const directText = Array.from(el.childNodes)
        .filter(node => node.nodeType === Node.TEXT_NODE)
        .map(node => node.textContent.trim())
        .join('').length;

    if (directText > maxTextLength) {
        maxTextLength = directText;
        bestElement = el;
    }

    // Check all descendants
    for (const desc of descendants) {
        const descStyle = window.getComputedStyle(desc);
        if (descStyle.display === 'none' || descStyle.visibility === 'hidden') {
            continue;
        }

        const descText = Array.from(desc.childNodes)
            .filter(node => node.nodeType === Node.TEXT_NODE)
            .map(node => node.textContent.trim())
            .join('').length;

        if (descText > maxTextLength) {
            maxTextLength = descText;
            bestElement = desc;
        }
    }

    return bestElement;
}

// Find the element with the most text content
textElement = findTextBearingElement(element);
textStyle = window.getComputedStyle(textElement);
const fgColor = textStyle.color;

Files Modified

  • daemon/daemon.go - Lines 9152-9220 (gradient analysis function)
  • daemon/daemon.go - Lines 9305-9340 (background detection logic)
  • daemon/daemon.go - Lines 9301-9354 (text color detection enhancement with recursive search)