Install
$ agentstack add skill-affaan-m-ecc-frontend-a11y ✓ 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.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Frontend Accessibility Patterns
Practical accessibility patterns for React and Next.js. Covers the issues most commonly flagged in code review: missing form labels, incorrect ARIA usage, non-semantic interactive elements, and broken keyboard navigation.
When to Activate
- Building or reviewing form components (`
,,`) - Creating interactive elements (modals, dropdowns, tooltips, tabs)
- Using `
orwithonClick` - Adding
aria-*attributes to any element - Implementing keyboard navigation or focus management
- Receiving accessibility feedback from code review tools (CodeRabbit, ESLint a11y)
- Building components that must support screen readers
Form Accessibility
Missing htmlFor / id pairing and disconnected error messages are the most common issues flagged in code review.
Label Connection
// BAD: label has no connection to input — screen readers cannot associate them
Email
// GOOD: htmlFor matches input id
Email
Required Fields
// BAD: visual-only asterisk conveys nothing to screen readers
Email *
// GOOD: required enables native browser validation; aria-required signals it to screen readers
Email *
Error Messages
// BAD: error text exists visually but is not linked to the input
Invalid email address
// GOOD: aria-describedby connects input to its error message
// aria-invalid signals the invalid state to screen readers
{error && (
{error}
)}
Complete Accessible Form
interface LoginFormProps {
onSubmit: (email: string, password: string) => void;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const newErrors: typeof errors = {};
if (!email) newErrors.email = 'Email is required';
if (!password) newErrors.password = 'Password is required';
if (Object.keys(newErrors).length) {
setErrors(newErrors);
return;
}
onSubmit(email, password);
};
return (
Email *
setEmail(e.target.value)}
aria-required="true"
aria-describedby={errors.email ? 'email-error' : undefined}
aria-invalid={!!errors.email}
autoComplete="email"
/>
{errors.email && (
{errors.email}
)}
Password *
setPassword(e.target.value)}
aria-required="true"
aria-describedby={errors.password ? 'password-error' : undefined}
aria-invalid={!!errors.password}
autoComplete="current-password"
/>
{errors.password && (
{errors.password}
)}
Log in
);
}
Semantic HTML
Use the element that matches the intent. Screen readers and keyboard users depend on native semantics.
// BAD: div has no role, no keyboard support, no accessible name
Submit
// GOOD: button is focusable, activates on Enter/Space, announces as "button"
Submit
// BAD: non-semantic navigation
navigate('/home')}>Home
// GOOD: anchor supports right-click, middle-click, and keyboard navigation
Home
// BAD: heading hierarchy skipped (h1 to h4)
Dashboard
Recent Activity
// GOOD: sequential heading levels
Dashboard
Recent Activity
ARIA Attributes
Use ARIA only when native HTML semantics are insufficient. Wrong ARIA is worse than no ARIA.
aria-label vs aria-labelledby
// aria-label: inline string label — use when no visible label text exists
// aria-labelledby: references another element's text — use when a visible label exists
Recent Orders
{/* content */}
aria-describedby
// Provides supplementary description beyond the label
Delete account
This action cannot be undone.
aria-live for Dynamic Content
// Use aria-live to announce content that updates without a page reload
// polite: waits for user to finish current action before announcing
// assertive: interrupts immediately — use only for urgent errors
export function StatusMessage({ message, isError }: { message: string; isError?: boolean }) {
return (
{message}
);
}
aria-expanded and aria-controls
export function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const contentId = useId();
return (
setIsOpen(prev => !prev)}>
{title}
{children}
);
}
Keyboard Navigation
Every interactive element must be reachable and operable by keyboard alone.
Custom Dropdown
export function Dropdown({ options, onSelect }: { options: string[]; onSelect: (value: string) => void }) {
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const listId = useId();
if (!options.length) return null;
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex(i => Math.min(i + 1, options.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex(i => Math.max(i - 1, 0));
break;
case 'Enter':
case ' ':
e.preventDefault();
if (isOpen) onSelect(options[activeIndex]);
setIsOpen(prev => !prev);
break;
case 'Escape':
setIsOpen(false);
break;
}
};
return (
setIsOpen(prev => !prev)}
>
{options[activeIndex]}
{isOpen && (
{options.map((option, index) => (
{
onSelect(option);
setIsOpen(false);
}}
>
{option}
))}
)}
);
}
Focus Management
Focus must move logically when UI state changes — especially for modals and route transitions.
Modal Focus Restoration
> This example covers initial focus and restoration. For a full focus trap (Tab/Shift+Tab cycling within the modal), use a library like focus-trap-react which handles edge cases like dynamic content and nested portals.
export function Modal({ isOpen, onClose, title, children }: { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode }) {
const modalRef = useRef(null);
const previousFocusRef = useRef(null);
useEffect(() => {
if (isOpen) {
// Save currently focused element and move focus into modal
previousFocusRef.current = document.activeElement as HTMLElement;
modalRef.current?.focus();
} else {
// Restore focus to the element that opened the modal
previousFocusRef.current?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
e.key === 'Escape' && onClose()}>
{title}
{children}
Close
);
}
Images and Icons
// BAD: decorative icon announced as unlabeled image
// GOOD: decorative image hidden from screen readers
// GOOD: meaningful image with descriptive alt text
// GOOD: icon button with accessible label
Reduced Motion
Respect users who have requested reduced motion in their OS settings.
export function useReducedMotion(): boolean {
const [prefersReduced, setPrefersReduced] = useState(false);
useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
setPrefersReduced(mq.matches);
const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return prefersReduced;
}
// Usage
export function AnimatedCard({ children }: { children: React.ReactNode }) {
const reduceMotion = useReducedMotion();
return (
{children}
);
}
Anti-Patterns
// BAD: onClick on non-interactive element with no keyboard support
Click me
// BAD: aria-label on a div that has no role
...
// BAD: placeholder used as a substitute for label
// BAD: positive tabIndex creates unpredictable tab order
Submit
// BAD: aria-hidden on a focusable element — keyboard users get trapped
Open
// BAD: role="button" on div without keyboard handler
Submit
// Missing: tabIndex={0}, onKeyDown for Enter/Space
Checklist
Before submitting any interactive component for review:
- [ ] Every `
,, andhas a connectedviahtmlFor/id` - [ ] Error messages are linked with
aria-describedbyand markedrole="alert" - [ ] No
onClickon `orwithoutrole,tabIndex, andonKeyDown` - [ ] Icon-only buttons have
aria-label - [ ] Decorative images use
alt=""andaria-hidden="true" - [ ] Modals restore focus on close (for full focus trapping with Tab/Shift+Tab cycling, use a library like
focus-trap-react) - [ ] Dynamic content updates use
aria-live - [ ]
prefers-reduced-motionis respected for animations
Related Skills
frontend-patterns— general React component and state patternsdesign-system— design token and component consistencymotion-ui— animation patterns with accessibility considerations
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: affaan-m
- Source: affaan-m/ECC
- License: MIT
- Homepage: https://ecc.tools
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.