accessibility-auditor
Audit UI components and pages for accessibility issues and suggest fixes to ensure Kibana is usable by everyone.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Audit UI components and pages for accessibility issues and suggest fixes to ensure Kibana is usable by everyone.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Self-evaluation loop for the treadmill Claude plugins pack. Runs cursor-plugin-evals against bundled skills, tracks quality over time, and escalates recurring failures via PAMS. Use periodically or before publishing plugin updates.
Autonomously implement technical plans from context/changes/<change-id>/plan.md under Codex's /goal — no human interaction at any point. Sibling of /shape-implement for unattended runs, in an interactive /goal session or headless via Codex -p. Flips the plan's Automated Progress rows, verifies each phase through an automatic quality-gate stack (plan success criteria, deliberate-break check, full suite), commits each phase on green with Conventional Commits, and surfaces pending Manual rows as a closing human checklist. Use when the user wants autonomous or unattended plan execution, pairs /goal with a plan, asks to "run the plan under /goal", or needs headless implementation.
Review implementation against plan for drift, dangerous decisions, and pattern compliance
Implement technical plans from context/changes/<change-id>/plan.md with verification
Review implementation plans for substance, feasibility, and architectural fitness. Use when user asks to review a plan, says "is this plan good", "check my plan", "review this plan", mentions plan review, or references a plan file and asks for feedback. Also trigger when user finishes /shape-plan and wants validation before starting /shape-implement.
Drive an approved implementation plan to completion phase by phase, test-first, through the red→green→refactor cycle, but only for phases whose implementation does not exist yet. Reads a plan from context/changes/<change-id>/plan.md and the canonical Progress section, and for each phase first checks whether the phase is TDD'able and still unimplemented — if it is, you write a failing test (RED), make it pass with the minimal code (GREEN), then clean up (REFACTOR); if it is not TDD'able, you redirect that phase to /shape-implement; if implementation is already present, you stop and explain that TDD does not work for already existing code, then suggest /shape-implement for that phase. Mirrors /shape-implement (same plan, same Progress source of truth, same phase-end commit ritual and clipboard handoffs) but flips the order so the failing test always comes before the code. Assumes test infrastructure is already in place — it does NOT set up runners, configs, fixtures, or CI. Use this skill when the user says "td
| name | accessibility-auditor |
| description | Audit UI components and pages for accessibility issues and suggest fixes to ensure Kibana is usable by everyone. |
Audit UI components and pages for accessibility (a11y) issues and suggest fixes to ensure Kibana is usable by everyone.
Scout provides built-in a11y checks via axe-core integration:
// In Scout test file (*.scout.ts)
import { expect, test } from '@kbn/scout';
test.describe('Accessibility', () => {
test('should have no a11y violations on main page', async ({ page }) => {
await page.goto('/app/my-feature');
// Wait for page to fully load
await page.waitForLoadState('networkidle');
// Run axe accessibility check
const violations = await page.checkA11y();
// Assert no violations
expect(violations).toHaveLength(0);
});
test('should have no violations in modal', async ({ page }) => {
await page.goto('/app/my-feature');
// Open modal
await page.getByRole('button', { name: 'Open Settings' }).click();
// Check modal specifically
const modal = page.getByRole('dialog');
await expect(modal).toBeVisible();
// Run a11y check on modal only
const violations = await page.checkA11y({ include: [['[role="dialog"]']] });
expect(violations).toHaveLength(0);
});
test('keyboard navigation should work', async ({ page }) => {
await page.goto('/app/my-feature');
// Tab through interactive elements
await page.keyboard.press('Tab');
await expect(page.getByRole('button', { name: 'First Button' })).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByRole('button', { name: 'Second Button' })).toBeFocused();
// Enter should activate focused button
await page.keyboard.press('Enter');
await expect(page.getByRole('dialog')).toBeVisible();
});
});
// ❌ BAD: Button without accessible name
<EuiButton onClick={handleClick}>
<EuiIcon type="trash" />
</EuiButton>
// ✅ GOOD: Button with aria-label
<EuiButton onClick={handleClick} aria-label="Delete item">
<EuiIcon type="trash" />
</EuiButton>
// ✅ BETTER: Button with visible text
<EuiButton onClick={handleClick} iconType="trash">
Delete
</EuiButton>
Detection:
# Search for buttons with only icons
grep -rn "EuiButton.*iconType" --include="*.tsx" | \
grep -v "aria-label" | \
grep -v "children"
// ❌ BAD: Insufficient contrast (< 4.5:1 for normal text)
<EuiText color="#999999">Important message</EuiText>
// ✅ GOOD: Use EUI color tokens (already WCAG compliant)
<EuiText color="danger">Important message</EuiText>
<EuiText color="subdued">Secondary text</EuiText>
Detection: Scout's checkA11y will flag contrast issues automatically.
// ❌ BAD: Input without label
<EuiFieldText
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
// ✅ GOOD: Use EuiFormRow for labels
<EuiFormRow label="Email address">
<EuiFieldText
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</EuiFormRow>
// ✅ ALTERNATIVE: aria-label if visual label not desired
<EuiFieldText
aria-label="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
Detection:
# Find form inputs without labels
grep -rn "EuiFieldText\|EuiFieldNumber\|EuiFieldPassword" --include="*.tsx" | \
while read -r line; do
file=$(echo "$line" | cut -d: -f1)
linenum=$(echo "$line" | cut -d: -f2)
# Check if wrapped in EuiFormRow or has aria-label
context=$(sed -n "$((linenum-5)),$((linenum+2))p" "$file")
if ! echo "$context" | grep -q "EuiFormRow\|aria-label"; then
echo "Missing label: $line"
fi
done
// ❌ BAD: Skip heading levels
<h1>Page Title</h1>
<h3>Section</h3> // Skipped h2
// ✅ GOOD: Proper hierarchy
<h1>Page Title</h1>
<h2>Main Section</h2>
<h3>Subsection</h3>
// ✅ Use EUI heading components
<EuiTitle size="l"><h1>Page Title</h1></EuiTitle>
<EuiTitle size="m"><h2>Main Section</h2></EuiTitle>
<EuiTitle size="s"><h3>Subsection</h3></EuiTitle>
// ✅ Ensure interactive elements are focusable
<div
role="button"
tabIndex={0} // Make focusable
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick();
}
}}
>
Custom button
</div>
// ✅ BETTER: Use native button or EUI component
<EuiButton onClick={handleClick}>
Native button (handles keyboard automatically)
</EuiButton>
// ✅ Use ARIA roles for custom widgets
<div role="tablist">
<button role="tab" aria-selected={activeTab === 0} aria-controls="panel-0">
Tab 1
</button>
<button role="tab" aria-selected={activeTab === 1} aria-controls="panel-1">
Tab 2
</button>
</div>
<div role="tabpanel" id="panel-0" hidden={activeTab !== 0}>
Panel 1 content
</div>
// ✅ Use aria-live for dynamic content
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
// Generated test file: my_feature.a11y.scout.ts
import { expect, test } from '@kbn/scout';
test.describe('My Feature - Accessibility', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/app/my-feature');
await page.waitForLoadState('networkidle');
});
test('main page should have no a11y violations', async ({ page }) => {
const violations = await page.checkA11y();
expect(violations).toHaveLength(0);
});
test('should have proper heading hierarchy', async ({ page }) => {
const h1Count = await page.locator('h1').count();
expect(h1Count).toBe(1); // Exactly one h1
// Verify heading levels don't skip
const headings = await page.locator('h1, h2, h3, h4, h5, h6').all();
const levels = await Promise.all(
headings.map(async (h) => {
const tagName = await h.evaluate((el) => el.tagName);
return parseInt(tagName[1]);
})
);
// Check no level skips (e.g., h1 -> h3)
for (let i = 1; i < levels.length; i++) {
const diff = levels[i] - levels[i - 1];
expect(diff).toBeLessThanOrEqual(1);
}
});
test('all interactive elements should be keyboard accessible', async ({ page }) => {
const buttons = page.locator('button, [role="button"]');
const buttonCount = await buttons.count();
for (let i = 0; i < buttonCount; i++) {
const button = buttons.nth(i);
// Check if focusable (tabIndex >= 0 or naturally focusable)
const tabIndex = await button.getAttribute('tabindex');
const tagName = await button.evaluate((el) => el.tagName);
if (tagName !== 'BUTTON' && (!tabIndex || parseInt(tabIndex) < 0)) {
const text = await button.textContent();
throw new Error(`Button "${text}" is not keyboard accessible`);
}
}
});
test('all images should have alt text', async ({ page }) => {
const images = await page.locator('img').all();
for (const img of images) {
const alt = await img.getAttribute('alt');
expect(alt).not.toBeNull();
// Decorative images should have empty alt
const role = await img.getAttribute('role');
if (role === 'presentation') {
expect(alt).toBe('');
}
}
});
test('form inputs should have labels', async ({ page }) => {
const inputs = await page.locator('input, textarea, select').all();
for (const input of inputs) {
const id = await input.getAttribute('id');
const ariaLabel = await input.getAttribute('aria-label');
const ariaLabelledBy = await input.getAttribute('aria-labelledby');
// Check for label (via for attribute, aria-label, or aria-labelledby)
const hasLabel =
ariaLabel ||
ariaLabelledBy ||
(id && (await page.locator(`label[for="${id}"]`).count()) > 0);
expect(hasLabel).toBeTruthy();
}
});
test('modals should trap focus', async ({ page }) => {
// Open modal
await page.getByRole('button', { name: 'Open Settings' }).click();
const modal = page.getByRole('dialog');
await expect(modal).toBeVisible();
// Find first and last focusable elements
const focusable = modal.locator(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusable.first();
const lastFocusable = focusable.last();
// Tab from last should go to first (focus trap)
await lastFocusable.focus();
await page.keyboard.press('Tab');
await expect(firstFocusable).toBeFocused();
// Shift+Tab from first should go to last
await page.keyboard.press('Shift+Tab');
await expect(lastFocusable).toBeFocused();
});
test('should support screen reader announcements', async ({ page }) => {
// Check for aria-live regions
const liveRegions = page.locator('[aria-live]');
expect(await liveRegions.count()).toBeGreaterThan(0);
// Verify toast notifications have role="status"
const toasts = page.locator('.euiToast');
for (let i = 0; i < (await toasts.count()); i++) {
const role = await toasts.nth(i).getAttribute('role');
expect(['status', 'alert']).toContain(role);
}
});
});
# Run Scout a11y tests
node scripts/scout run-tests \
--arch stateful \
--domain classic \
--testFiles "x-pack/test/scout/features/my_feature/my_feature.a11y.scout.ts"
# Run on multiple pages
node scripts/scout run-tests \
--arch stateful \
--domain classic \
--config "x-pack/test/scout/config/stateful/accessibility.config.ts"
Beyond automated tests, perform manual checks:
## Manual Accessibility Testing
### Keyboard Navigation
- [ ] Tab through all interactive elements
- [ ] Shift+Tab works in reverse
- [ ] Enter/Space activates buttons
- [ ] Arrow keys work in custom widgets (tabs, menus)
- [ ] Focus indicator is visible
- [ ] Focus order is logical (left-to-right, top-to-bottom)
- [ ] Modal/dialog traps focus
### Screen Reader (VoiceOver/NVDA/JAWS)
- [ ] All content is announced
- [ ] Form labels are read correctly
- [ ] Button purposes are clear
- [ ] Error messages are announced
- [ ] Status updates are announced (aria-live)
- [ ] Images have meaningful alt text
- [ ] Headings provide page structure
### Visual
- [ ] Text meets contrast requirements (4.5:1 for normal, 3:1 for large)
- [ ] Focus indicator is visible
- [ ] Color is not the only indicator (use icons + color)
- [ ] UI is usable at 200% zoom
- [ ] Text can be resized without breaking layout
### Cognitive
- [ ] Error messages are clear and actionable
- [ ] Instructions are easy to understand
- [ ] Timeout warnings are provided
- [ ] Consistent navigation patterns
#!/bin/bash
# quick_a11y_fix.sh - Fix common a11y issues
file="$1"
if [ ! -f "$file" ]; then
echo "Usage: $0 <file.tsx>"
exit 1
fi
echo "Scanning $file for common a11y issues..."
# Check 1: Buttons with only icons (missing labels)
if grep -q "EuiButton.*iconType" "$file"; then
echo ""
echo "⚠️ Found buttons with only icons (may need aria-label):"
grep -n "EuiButton.*iconType" "$file" | grep -v "aria-label" | grep -v "children"
echo ""
echo "Fix: Add aria-label or visible text"
echo " <EuiButton iconType=\"trash\" aria-label=\"Delete item\" />"
fi
# Check 2: Form inputs without labels
if grep -E -q "EuiFieldText|EuiFieldNumber|EuiFieldPassword" "$file"; then
echo ""
echo "⚠️ Found form inputs (verify they have labels):"
grep -n -E "EuiFieldText|EuiFieldNumber|EuiFieldPassword" "$file"
echo ""
echo "Fix: Wrap in EuiFormRow or add aria-label"
fi
# Check 3: Custom onClick handlers (may need keyboard support)
if grep -q "onClick.*<div\|onClick.*<span" "$file"; then
echo ""
echo "⚠️ Found onClick on non-button elements (may need keyboard support):"
grep -n "onClick.*<div\|onClick.*<span" "$file"
echo ""
echo "Fix: Add role, tabIndex, and onKeyDown"
echo " <div role=\"button\" tabIndex={0} onKeyDown={handleKeyDown} onClick={handleClick} />"
fi
echo ""
echo "Run Scout a11y test to verify:"
echo " node scripts/scout run-tests --testFiles path/to/test.a11y.scout.ts"
Step 1: Generate a11y test suite
cat > x-pack/test/scout/features/my_feature/my_feature.a11y.scout.ts <<'EOF'
[Generated test content from template above]
EOF
Step 2: Run Scout a11y tests
node scripts/scout run-tests \
--arch stateful \
--domain classic \
--testFiles "x-pack/test/scout/features/my_feature/my_feature.a11y.scout.ts"
Step 3: Parse violations
Found 3 accessibility violations:
1. Missing aria-label on icon button (line 45)
- Element: <EuiButton iconType="trash" />
- Fix: Add aria-label="Delete item"
2. Insufficient color contrast (line 78)
- Element: <EuiText color="#999999">
- Contrast: 3.2:1 (needs 4.5:1)
- Fix: Use color="subdued" or darker color
3. Missing form label (line 102)
- Element: <EuiFieldText />
- Fix: Wrap in <EuiFormRow label="Email address">
Step 4: Apply fixes
// Fix 1: Add aria-label
- <EuiButton iconType="trash" onClick={handleDelete} />
+ <EuiButton iconType="trash" onClick={handleDelete} aria-label="Delete item" />
// Fix 2: Use EUI color token
- <EuiText color="#999999">Secondary text</EuiText>
+ <EuiText color="subdued">Secondary text</EuiText>
// Fix 3: Add form label
- <EuiFieldText value={email} onChange={handleChange} />
+ <EuiFormRow label="Email address">
+ <EuiFieldText value={email} onChange={handleChange} />
+ </EuiFormRow>
Step 5: Re-run tests
node scripts/scout run-tests --testFiles my_feature.a11y.scout.ts
# Output: All tests passed ✓