Install
$ agentstack add skill-marvinrichter-clarc-accessibility-patterns ✓ 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 Patterns
Build accessible UIs that work for all users — keyboard, screen readers, and beyond.
When to Activate
- Building or reviewing interactive components (modals, forms, dropdowns, tabs)
- Running accessibility audits on existing code
- Adding automated a11y tests with axe-core or jest-axe
- Implementing keyboard navigation or focus management
- Checking WCAG 2.2 compliance
- Adding ARIA attributes to custom components
WCAG 2.2 — Key Success Criteria
| Criterion | Level | What it means | |-----------|-------|---------------| | 1.1.1 Non-text Content | A | All images need alt text or aria-label | | 1.3.1 Info and Relationships | A | Use semantic HTML — headings, lists, landmarks | | 1.4.3 Contrast Minimum | AA | Text: 4.5:1 ratio; large text (≥18pt): 3:1 | | 1.4.11 Non-text Contrast | AA | UI components and graphics: 3:1 ratio | | 2.1.1 Keyboard | A | Everything operable via keyboard | | 2.4.3 Focus Order | A | Tab order is logical and meaningful | | 2.4.7 Focus Visible | AA | Keyboard focus is always visible | | 2.4.11 Focus Appearance | AA | (WCAG 2.2 new) Focus indicator: ≥2px, ≥3:1 contrast | | 2.5.8 Target Size | AA | (WCAG 2.2 new) Touch targets ≥24×24 CSS px | | 4.1.2 Name, Role, Value | A | ARIA attributes are correct and complete |
ARIA Patterns
Labels
...
Confirm Delete
...
Use your company email address.
This field is required.
Roles
Custom Button
...
...
...
...
...
Live Regions
3 results found
Error: Payment failed. Please try again.
Saving...
State Attributes
Menu
...
Bold
Details
Email address is required.
Keyboard Navigation
Focus Trap (Modals/Drawers)
// Focus trap — keep focus inside modal while open
function trapFocus(container: HTMLElement): () => void {
const focusableSelectors = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
].join(', ');
const focusableElements = Array.from(
container.querySelectorAll(focusableSelectors)
);
const firstElement = focusableElements[0];
const lastElement = focusableElements.at(-1)!;
// Move focus into modal on open
firstElement?.focus();
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
// Shift+Tab: if on first element, wrap to last
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
// Tab: if on last element, wrap to first
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
};
container.addEventListener('keydown', handleKeyDown);
return () => container.removeEventListener('keydown', handleKeyDown);
}
// React hook
function useFocusTrap(isOpen: boolean, containerRef: RefObject) {
useEffect(() => {
if (!isOpen || !containerRef.current) return;
const previouslyFocused = document.activeElement as HTMLElement;
const cleanup = trapFocus(containerRef.current);
return () => {
cleanup();
// Restore focus when modal closes
previouslyFocused?.focus();
};
}, [isOpen]);
}
Skip Links
Skip to main content
...
/* Visible only when focused */
.skip-link {
position: absolute;
top: -40px;
left: 6px;
z-index: 9999;
padding: 8px;
background: #000;
color: #fff;
text-decoration: none;
}
.skip-link:focus {
top: 6px;
}
Focus Management on Route Change (SPA)
// Restore focus to page heading on every route change
function RouterFocusManager() {
const pathname = usePathname();
const headingRef = useRef(null);
useEffect(() => {
headingRef.current?.focus();
}, [pathname]);
return {getPageTitle(pathname)};
}
WAI-ARIA Menu Button (Dropdowns)
// Full keyboard support: Enter/Space/ArrowDown opens, ArrowUp/Down/Home/End navigate, Escape closes
function DropdownMenu({ trigger, items }: DropdownMenuProps) {
const [open, setOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const handleKeyDown = (e: KeyboardEvent) => {
if (!open) {
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
e.preventDefault();
setOpen(true);
setActiveIndex(0);
}
return;
}
switch (e.key) {
case 'ArrowDown': e.preventDefault(); setActiveIndex(i => Math.min(i + 1, items.length - 1)); break;
case 'ArrowUp': e.preventDefault(); setActiveIndex(i => Math.max(i - 1, 0)); break;
case 'Home': e.preventDefault(); setActiveIndex(0); break;
case 'End': e.preventDefault(); setActiveIndex(items.length - 1); break;
case 'Escape': setOpen(false); break;
case 'Tab': setOpen(false); break;
}
};
return (
setOpen(!open)}>
Options
{open && (
{items.map((item, i) => (
{ item.onSelect(); setOpen(false); }}>
{item.label}
))}
)}
);
}
Roving tabindex (Radio Groups, Toolbars)
// Only one item in group has tabindex="0" at a time
// Arrow keys move focus within the group
function RovingTabGroup({ items }: { items: string[] }) {
const [activeIndex, setActiveIndex] = useState(0);
const handleKeyDown = (e: KeyboardEvent, index: number) => {
let next = index;
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
next = (index + 1) % items.length;
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
next = (index - 1 + items.length) % items.length;
} else if (e.key === 'Home') {
next = 0;
} else if (e.key === 'End') {
next = items.length - 1;
} else {
return;
}
e.preventDefault();
setActiveIndex(next);
itemRefs[next]?.focus();
};
return (
{items.map((item, i) => (
handleKeyDown(e, i)}
ref={(el) => { itemRefs[i] = el; }}
>
{item}
))}
);
}
Escape Key Convention
// All overlays (modals, drawers, dropdowns, tooltips) must close on Escape
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen, onClose]);
For screen reader testing checklists (NVDA/VoiceOver), automated testing (axe-core, jest-axe, @axe-core/react), and accessible component patterns (modal, table, form, data visualization), see skill accessibility-patterns-advanced.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: marvinrichter
- Source: marvinrichter/clarc
- License: MIT
- Homepage: https://marvinrichter.github.io/clarc
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.