Install
$ agentstack add skill-medy-gribkov-arcana-accessibility-wcag ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Accessibility & WCAG Compliance
Audit web applications for WCAG 2.1/2.2 compliance, fix accessibility barriers, implement proper semantic HTML and ARIA patterns.
BAD: Common Accessibility Violations
// BAD: Div button without keyboard support
Submit
// BAD: Color-only error indication
Error
// BAD: Image without alt text
// BAD: Aria-label overuse
Click me
// BAD: Auto-focus abuse
{/* Steals focus */}
// BAD: No heading hierarchy
Page Title
Section {/* Skips h2, h3 */}
// BAD: Form without labels
Choose...
// BAD: Custom dropdown without keyboard support
{options.map(opt => select(opt)}>{opt})}
GOOD: Accessible Implementations
// GOOD: Semantic button with proper keyboard support
Submit
// GOOD: Multi-sensory error indication
Error:
Invalid email format
// GOOD: Descriptive alt text (or empty for decorative)
// GOOD: Redundant aria-label removed
Click me
// GOOD: Managed focus with trap
function Modal({ open, onClose, children }) {
const modalRef = useRef(null);
useEffect(() => {
if (!open) return;
const previousFocus = document.activeElement as HTMLElement;
modalRef.current?.focus();
return () => previousFocus?.focus();
}, [open]);
return (
{
if (e.key === 'Escape') onClose();
}}
>
{children}
);
}
// GOOD: Proper heading hierarchy
Page Title
Main Section
Subsection
Another Section
// GOOD: Labeled form controls
Email address
We'll never share your email
Country
Select a country
United States
// GOOD: Accessible custom dropdown
function Dropdown({ options, value, onChange, label }) {
const [open, setOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const buttonRef = useRef(null);
const listRef = useRef(null);
const handleKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex(prev => Math.min(prev + 1, options.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex(prev => Math.max(prev - 1, 0));
break;
case 'Enter':
case ' ':
e.preventDefault();
onChange(options[activeIndex]);
setOpen(false);
buttonRef.current?.focus();
break;
case 'Escape':
setOpen(false);
buttonRef.current?.focus();
break;
}
};
return (
setOpen(!open)}
>
{label}: {value}
{open && (
{options.map((opt, i) => (
{
onChange(opt);
setOpen(false);
buttonRef.current?.focus();
}}
>
{opt}
))}
)}
);
}
Keyboard Navigation Patterns
// Roving tabindex for toolbar
function Toolbar({ items }) {
const [focusedIndex, setFocusedIndex] = useState(0);
const handleKeyDown = (e: KeyboardEvent, index: number) => {
let newIndex = index;
if (e.key === 'ArrowRight') {
newIndex = (index + 1) % items.length;
} else if (e.key === 'ArrowLeft') {
newIndex = (index - 1 + items.length) % items.length;
} else if (e.key === 'Home') {
newIndex = 0;
} else if (e.key === 'End') {
newIndex = items.length - 1;
} else {
return;
}
e.preventDefault();
setFocusedIndex(newIndex);
};
return (
{items.map((item, i) => (
handleKeyDown(e, i)}
onFocus={() => setFocusedIndex(i)}
>
{item.label}
))}
);
}
// Skip navigation link
function Layout({ children }) {
return (
<>
Skip to main content
{/* Nav items */}
{children}
);
}
// Focus-visible styling (CSS)
/* Show focus only for keyboard users */
button:focus {
outline: none;
}
button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
Live Regions and Dynamic Content
// Status announcements
function Toast({ message, type }) {
return (
{message}
);
}
// Urgent alerts
function ErrorAlert({ message }) {
return (
Error: {message}
);
}
// Loading state announcement
function DataTable({ loading, data }) {
return (
{loading && (
Loading data...
)}
Sales data for 2025
Month
Revenue
{data.map(row => (
{row.month}
{row.revenue}
))}
);
}
Color Contrast and Motion
/* GOOD: WCAG AA contrast ratios */
.text-normal {
color: #1a1a1a; /* 4.5:1 on white */
background: #ffffff;
}
.text-large {
font-size: 18px;
font-weight: 700;
color: #4a4a4a; /* 3:1 on white (large text) */
}
.button-primary {
background: #0066cc; /* 4.5:1 with white text */
color: #ffffff;
}
/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* GOOD: Graceful animation degradation */
@media (prefers-reduced-motion: no-preference) {
.fade-in {
animation: fadeIn 0.3s ease-in;
}
}
@media (prefers-reduced-motion: reduce) {
.fade-in {
opacity: 1; /* Skip animation, show immediately */
}
}
Automated Testing
// axe-core integration (Jest + React Testing Library)
import { axe, toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';
expect.extend(toHaveNoViolations);
test('LoginForm has no accessibility violations', async () => {
const { container } = render();
const results = await axe(container);
expect(results).toHaveNoViolations();
});
// Playwright accessibility audit
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('Homepage should not have accessibility violations', async ({ page }) => {
await page.goto('http://localhost:3000');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(results.violations).toEqual([]);
});
// Cypress accessibility testing
describe('Accessibility', () => {
beforeEach(() => {
cy.visit('/');
cy.injectAxe();
});
it('Has no detectable accessibility violations', () => {
cy.checkA11y();
});
it('Has no violations on specific component', () => {
cy.checkA11y('.navigation-menu');
});
it('Respects wcag2aa standard', () => {
cy.checkA11y(null, {
runOnly: {
type: 'tag',
values: ['wcag2aa']
}
});
});
});
Workflow
When auditing accessibility:
- Run automated tools first: axe DevTools, Lighthouse, WAVE
- Check keyboard navigation: Tab through entire page, verify focus order
- Test with screen reader: NVDA (Windows), VoiceOver (Mac), JAWS
- Verify color contrast: Use contrast checker for all text/UI elements
- Validate ARIA: Ensure roles/states match component behavior
- Test dynamic content: Live regions announce properly
- Check form accessibility: Labels, errors, validation messages
- Verify responsive behavior: Touch targets >= 44x44px on mobile
Common WCAG 2.1 Level AA failures:
- Missing alt text (1.1.1)
- Insufficient contrast (1.4.3)
- No keyboard access (2.1.1)
- Missing form labels (3.3.2)
- Invalid HTML/ARIA (4.1.1, 4.1.2)
Fix priority: Critical (blocks screen readers) > High (keyboard issues) > Medium (contrast) > Low (enhancements).
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: medy-gribkov
- Source: medy-gribkov/arcana
- License: Apache-2.0
- Homepage: https://www.npmjs.com/package/@sporesec/arcana
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.