I use Claude's vision API to review UI screenshots for accessibility issues
← Back
April 4, 2026Claude5 min read

I use Claude's vision API to review UI screenshots for accessibility issues

Published April 4, 20265 min read

I discovered that Claude can review UI screenshots for accessibility issues when I pasted a login form screenshot and jokingly asked if it would pass WCAG. It gave back five specific issues including exact hex colors and contrast ratios. I now run this before every UI component ships. It catches issues that automated tools like Lighthouse miss.

The review script

python
import anthropic
import base64
from pathlib import Path

client = anthropic.Anthropic()

A11Y_SYSTEM = """You are an accessibility expert reviewing UI screenshots against WCAG 2.1 AA standards.

For each screenshot, analyze:
1. COLOR CONTRAST: Identify text with insufficient contrast (< 4.5:1 for normal text, < 3:1 for large text)
2. FOCUS INDICATORS: Check if interactive elements have visible focus states
3. TEXT SIZE: Flag text that appears smaller than 16px (may be hard to read)
4. BUTTON LABELS: Check for buttons/links with unclear labels ("click here", unlabeled icons)
5. FORM LABELS: Check if form inputs have visible, associated labels
6. ERROR STATES: Check if errors are communicated with more than just color
7. TOUCH TARGETS: Check if interactive elements appear to be at least 44x44px

Format findings as:
**[WCAG Level: A/AA/AAA] [Issue Type]**: Description
Location: Where in the screenshot
Severity: Critical/High/Medium/Low
Fix: Specific change needed

If the UI looks clean, say so — don't manufacture issues."""


def review_screenshot(image_path: str) -> str:
    image_data = base64.standard_b64encode(
        Path(image_path).read_bytes()
    ).decode("utf-8")
    
    # Determine media type from extension
    ext = Path(image_path).suffix.lower()
    media_type = {
        '.jpg': 'image/jpeg',
        '.jpeg': 'image/jpeg',
        '.png': 'image/png',
        '.webp': 'image/webp',
        '.gif': 'image/gif',
    }.get(ext, 'image/png')
    
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=1500,
        system=A11Y_SYSTEM,
        messages=[{
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": media_type,
                        "data": image_data,
                    },
                },
                {
                    "type": "text",
                    "text": "Review this UI for accessibility issues."
                }
            ],
        }]
    )
    return response.content[0].text


if __name__ == "__main__":
    import sys
    for image_path in sys.argv[1:]:
        print(f"
## Review: {image_path}
")
        print(review_screenshot(image_path))

Reviewing multiple states at once

python
def review_component_states(
    screenshots: dict[str, str],  # {state_name: image_path}
) -> str:
    """Review multiple UI states in one call."""
    
    content = []
    for state_name, image_path in screenshots.items():
        image_data = base64.standard_b64encode(
            Path(image_path).read_bytes()
        ).decode("utf-8")
        
        content.append({
            "type": "text",
            "text": f"=== {state_name} state ==="
        })
        content.append({
            "type": "image",
            "source": {
                "type": "base64",
                "media_type": "image/png",
                "data": image_data,
            }
        })
    
    content.append({
        "type": "text",
        "text": "Review all these UI states for accessibility issues. Note which state each issue appears in."
    })
    
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=2000,
        system=A11Y_SYSTEM,
        messages=[{"role": "user", "content": content}]
    )
    return response.content[0].text

# Usage
review = review_component_states({
    "default": "screenshots/button-default.png",
    "hover": "screenshots/button-hover.png",
    "disabled": "screenshots/button-disabled.png",
    "focus": "screenshots/button-focus.png",
})

Integrating with Playwright screenshots

typescript
import { test } from '@playwright/test';
import { execSync } from 'child_process';
import path from 'path';

test('login form accessibility', async ({ page }) => {
  await page.goto('/login');
  
  // Take screenshot
  const screenshotPath = path.join(__dirname, 'screenshots/login.png');
  await page.screenshot({ path: screenshotPath });
  
  // Run AI accessibility review
  const review = execSync(
    `python3 scripts/a11y-review.py ${screenshotPath}`,
    { encoding: 'utf-8' }
  );
  
  console.log('Accessibility Review:', review);
  
  // Fail test if CRITICAL issues found
  if (review.includes('[WCAG Level: A]') && review.includes('Critical')) {
    throw new Error(`Critical accessibility issue found:
${review}`);
  }
});

What Claude catches vs automated tools

Automated tools like axe-core and Lighthouse catch structural accessibility issues — missing alt text, wrong ARIA roles, missing form labels. Claude catches visual accessibility issues that automated tools cannot check from HTML alone:

  • Text that is technically labeled but the label is visually far from the input
  • Buttons that look disabled but are not marked as disabled in the HTML
  • Icons that are technically labeled but the label is invisible to sighted users
  • Visual focus indicators that exist in the DOM but are invisible against the background

The best workflow: run axe-core in your test suite for structural issues, run Claude vision for visual issues. Together they cover both the DOM and the rendered output.

Share this
← All Posts5 min read