Skip to main content
ClaudeWave
Skill241 repo starsupdated 2mo ago

accessibility-audit

|

Install in Claude Code
Copy
git clone --depth 1 https://github.com/billy-enrizky/openbrowser-ai /tmp/accessibility-audit && cp -r /tmp/accessibility-audit/plugin/skills/accessibility-audit ~/.claude/skills/accessibility-audit
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Accessibility Audit

Audit web pages for accessibility issues following WCAG 2.1 guidelines using Python code execution. Checks heading structure, form labels, image alt text, ARIA attributes, landmark regions, and keyboard navigation.

All code runs via `openbrowser-ai -c`. The daemon starts automatically and persists variables across calls. All browser functions are async -- use `await`.

The CLI daemon also persists cookies and login state in `~/.config/openbrowser/profiles/daemon/storage_state.json`, so authenticated sessions can be reused across later runs.

## Setup

Before running, verify openbrowser-ai is installed:

```bash
openbrowser-ai --help
```

If not found, install:

```bash
# macOS/Linux
curl -fsSL https://raw.githubusercontent.com/billy-enrizky/openbrowser-ai/main/install.sh | sh

# Windows (PowerShell)
irm https://raw.githubusercontent.com/billy-enrizky/openbrowser-ai/main/install.ps1 | iex
```

## Workflow

### Step 1 -- Navigate and initialize audit

```bash
openbrowser-ai -c - <<'EOF'
await navigate("https://example.com")
state = await browser.get_browser_state_summary()
print(f"Auditing: {state.title} ({state.url})")

# Store all findings
audit = {
    "url": state.url,
    "title": state.title,
    "issues": [],
    "checks": {}
}
EOF
```

### Step 2 -- Check heading structure

```bash
openbrowser-ai -c - <<'EOF'
headings_result = await evaluate("""
(function(){
  const headings = Array.from(document.querySelectorAll("h1,h2,h3,h4,h5,h6"));
  const issues = [];
  let prevLevel = 0;
  const h1Count = headings.filter(h => h.tagName === "H1").length;

  if (h1Count === 0) issues.push("No h1 element found");
  if (h1Count > 1) issues.push("Multiple h1 elements: " + h1Count);

  headings.forEach(h => {
    const level = parseInt(h.tagName[1]);
    if (prevLevel > 0 && level > prevLevel + 1)
      issues.push("Skipped level: h" + prevLevel + " -> h" + level + " (\"" + h.textContent.trim().substring(0, 50) + "\")");
    if (!h.textContent.trim())
      issues.push("Empty heading: " + h.tagName);
    prevLevel = level;
  });

  return {
    total: headings.length,
    h1Count,
    hierarchy: headings.map(h => ({ tag: h.tagName, text: h.textContent.trim().substring(0, 80) })),
    issues
  };
})()
""")

audit["checks"]["headings"] = headings_result
for issue in headings_result.get("issues", []):
    audit["issues"].append({"check": "headings", "wcag": "1.3.1", "issue": issue})
    print(f"[HEADINGS] {issue}")

if not headings_result.get("issues"):
    print("[HEADINGS] PASS")
EOF
```

### Step 3 -- Check images for alt text

```bash
openbrowser-ai -c - <<'EOF'
images_result = await evaluate("""
(function(){
  const images = Array.from(document.querySelectorAll("img"));
  const issues = [];
  let withAlt = 0, withEmptyAlt = 0, missingAlt = 0;

  images.forEach(img => {
    const alt = img.getAttribute("alt");
    const src = img.src?.substring(0, 100);
    if (alt === null) {
      missingAlt++;
      issues.push("Missing alt: " + src);
    } else if (alt === "") {
      withEmptyAlt++;
    } else {
      withAlt++;
    }
  });

  return { total: images.length, withAlt, withEmptyAlt, missingAlt, issues };
})()
""")

audit["checks"]["images"] = images_result
for issue in images_result.get("issues", []):
    audit["issues"].append({"check": "images", "wcag": "1.1.1", "issue": issue})
    print(f"[IMAGES] {issue}")

if not images_result.get("issues"):
    total = images_result["total"]
    with_alt = images_result["withAlt"]
    print(f"[IMAGES] PASS ({total} images, {with_alt} with alt)")
EOF
```

### Step 4 -- Check form labels

```bash
openbrowser-ai -c - <<'EOF'
forms_result = await evaluate("""
(function(){
  const inputs = Array.from(document.querySelectorAll("input:not([type=\"hidden\"]),select,textarea"));
  const issues = [];

  inputs.forEach(input => {
    const id = input.id;
    const ariaLabel = input.getAttribute("aria-label");
    const ariaLabelledBy = input.getAttribute("aria-labelledby");
    const title = input.getAttribute("title");
    const label = id ? document.querySelector("label[for=\"" + id + "\"]") : null;
    const parentLabel = input.closest("label");
    const hasLabel = label || parentLabel || ariaLabel || ariaLabelledBy || title;

    if (!hasLabel) {
      issues.push({
        tag: input.tagName,
        type: input.type || "text",
        name: input.name || "(none)",
        placeholder: input.getAttribute("placeholder") || "(none)",
        issue: "No label or aria-label"
      });
    }
  });

  return { totalInputs: inputs.length, unlabeled: issues.length, issues };
})()
""")

audit["checks"]["forms"] = forms_result
for issue in forms_result.get("issues", []):
    tag = issue["tag"]
    name = issue["name"]
    itype = issue["type"]
    audit["issues"].append({"check": "forms", "wcag": "1.3.1", "issue": f"Unlabeled {tag} name={name}"})
    print(f"[FORMS] Unlabeled: <{tag}> type={itype} name={name}")

if not forms_result.get("issues"):
    total_inputs = forms_result["totalInputs"]
    print(f"[FORMS] PASS ({total_inputs} inputs, all labeled)")
EOF
```

### Step 5 -- Check ARIA attributes

```bash
openbrowser-ai -c - <<'EOF'
aria_result = await evaluate("""
(function(){
  const issues = [];
  const ariaElements = document.querySelectorAll("[role],[aria-label],[aria-labelledby],[aria-describedby],[aria-hidden]");

  ariaElements.forEach(el => {
    const ariaLabelledBy = el.getAttribute("aria-labelledby");
    const ariaDescribedBy = el.getAttribute("aria-describedby");

    if (ariaLabelledBy) {
      ariaLabelledBy.split(/\s+/).forEach(id => {
        if (!document.getElementById(id))
          issues.push({ element: el.tagName, issue: "aria-labelledby references missing id: " + id });
      });
    }
    if (ariaDescribedBy) {
      ariaDescribedBy.split(/\s+/).forEach(id => {
        if (!document.getElementById(id))
          issues.push({ element: el.tagName, issue: "aria-describedby references missing id: " + id });
      });