Accessibility pattern enforcement for TypeScript (TSX) and JavaScript (JSX). Detects missing alt text, non-semantic HTML, absent ARIA roles, keyboard-inaccessible interactive elements, and click-only event handlers. Wired into the conductor's review operation.
From clean-code-codexnpx claudepluginhub mikecubed/agent-orchestration --plugin clean-code-codexThis skill uses the workspace's default tool permissions.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Details PluginEval's skill quality evaluation: 3 layers (static, LLM judge), 10 dimensions, rubrics, formulas, anti-patterns, badges. Use to interpret scores, improve triggering, calibrate thresholds.
Scope: This skill applies only to files containing JSX/TSX syntax (
.tsx,.jsx, files with<JSX tags). Skip plain.ts/.jsfiles with no JSX content.
Precedence in the overall system: SEC → TDD → ARCH/TYPE → A11Y-1 (BLOCK) → A11Y-2 through A11Y-5.
Severity: BLOCK | Languages: TypeScript (TSX), JavaScript (JSX) | Source: CCC
What it prohibits: <img> elements and supported image components (e.g. Next.js
<Image>) without an alt attribute at all.
Decorative images with alt="" are valid per W3C spec and must not be flagged.
Prohibited patterns:
// No alt attribute at all
<img src="/hero.png" />
// Next.js Image component with no alt
<Image src="/hero.png" width={800} height={600} />
Exemptions:
<img alt="" /> alone is valid for decorative images per W3C spec — do not flag<img alt="" role="presentation" /> is also valid; role="presentation" is optional, not requiredaria-hidden="true" are not covered by this ruleDetection:
<img or <Image tags in .tsx and .jsx filesalt= attributealt="" is present: accept it as a valid decorative image — do not flagalt attribute entirelyagent_action:
A11Y-1 (BLOCK): Image at {file}:{line} has no alt text — screen readers cannot describe it.// Meaningful image: add descriptive alt text
<img src="/hero.png" alt="Team collaborating around a whiteboard" />
// Decorative image: alt="" alone is sufficient
<img src="/divider.png" alt="" />
--fix: add a placeholder alt="TODO: describe image" — require human
to fill in the actual descriptionBypass prohibition: "It's just a decorative image" without any alt attribute
→ Refuse. Cite A11Y-1. Decorative images must have alt="" to be valid.
role="presentation" is optional but alt="" is the minimum requirement.
Severity: WARN | Languages: TypeScript (TSX), JavaScript (JSX) | Source: CCC
What it prohibits: Using <div> or <span> for elements that have semantic
equivalents. Interactive elements built from generic divs lose keyboard
accessibility and screen reader meaning.
Prohibited patterns:
// div used as a button — loses keyboard accessibility
<div onClick={handleClick}>Submit</div>
// div with a role that has a native element
<div role="navigation">...</div> // use <nav>
<div role="main">...</div> // use <main>
<div role="banner">...</div> // use <header>
<div role="contentinfo">...</div> // use <footer>
// span used as interactive element
<span onClick={handleClick}>Click me</span>
Exemptions:
<div> elements used purely for styling containers (flexbox/grid wrappers)Detection:
<div onClick, <div role=, <span onClick in .tsx and .jsx files<button> or <a>agent_action:
A11Y-2 (WARN): Non-semantic element at {file}:{line} — <div> used as interactive control.| Current | Replacement |
|---|---|
<div onClick=...> | <button onClick=...> |
<div role="navigation"> | <nav> |
<div role="main"> | <main> |
<div role="banner"> | <header> |
<div role="contentinfo"> | <footer> |
--fix: replace the element with its semantic equivalent — preserve all
existing props and childrenSeverity: WARN | Languages: TypeScript (TSX), JavaScript (JSX) | Source: CCC
What it prohibits: Custom interactive components (dropdowns, modals, tabs,
accordions) that have no ARIA role, no aria-label/aria-labelledby, and no
aria-expanded/aria-selected state management.
Prohibited patterns:
// Custom dropdown with no ARIA attributes
<div className="dropdown" onClick={toggleOpen}>
{isOpen && <ul className="dropdown-menu">{items}</ul>}
</div>
// Tab panel with no role or labelling
<div className="tab-panel">{content}</div>
// Modal overlay with no dialog role
<div className="modal-overlay">
<div className="modal-content">{children}</div>
</div>
Required patterns:
// Dropdown with proper ARIA
<div role="combobox" aria-expanded={isOpen} aria-label="Select option">
{isOpen && <ul role="listbox">{items}</ul>}
</div>
// Tab panel with role and labelling
<div role="tabpanel" aria-labelledby="tab-1">{content}</div>
// Modal with dialog role
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
<h2 id="modal-title">Confirm Action</h2>
{children}
</div>
Exemptions:
<select> wrapped in a styled container)Detection:
Modal, Dropdown, Accordion,
Tab, Tooltip, Popover, Dialog, Menu) in component definitionsrole=, aria-label,
aria-labelledby, or aria-expandedagent_action:
A11Y-3 (WARN): Custom interactive component at {file}:{line} has no ARIA attributes.| Component | Minimum ARIA |
|---|---|
| Dropdown | role="combobox", aria-expanded |
| Modal | role="dialog", aria-modal="true", aria-labelledby |
| Tab | role="tab", aria-selected |
| Tab Panel | role="tabpanel", aria-labelledby |
| Accordion | role="region", aria-expanded on trigger |
| Tooltip | role="tooltip", trigger gets aria-describedby |
--fix: add the minimum ARIA attributes to the component root elementSeverity: BLOCK | Languages: TypeScript (TSX), JavaScript (JSX) | Source: CCC
What it prohibits: Elements with click handlers but no keyboard handler
(onKeyDown, onKeyUp, onKeyPress) and no tabIndex. Keyboard users
cannot reach or activate these elements.
Prohibited patterns:
// div with click handler but no keyboard support
<div onClick={handleClick}>Click me</div>
// span with click handler but no keyboard support
<span onClick={handleAction} className="link">Learn more</span>
// Custom card component with click but no keyboard access
<div onClick={() => navigate('/details')} className="card">
<h3>{title}</h3>
<p>{description}</p>
</div>
Exemptions:
disabled attribute or aria-disabled="true")<button> and <a href> elements (inherently keyboard accessible)role="presentation" that are not meant to be interactiveDetection:
onClick={ on non-button/non-anchor elements in .tsx and .jsx filesonKeyDown, onKeyUp, or
onKeyPress attributestabIndex attributeonClick but no keyboard equivalent and no tabIndexagent_action:
A11Y-4 (BLOCK): Element at {file}:{line} has onClick but no keyboard handler — keyboard users cannot activate it.// Best: convert to semantic button
<button onClick={handleClick}>Click me</button>
// If button is not appropriate: add tabIndex and keyboard handler
<div
onClick={handleClick}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleClick(e); }}
tabIndex={0}
role="button"
>
Click me
</div>
--fix: prefer converting to <button> over adding tabIndex +
onKeyDown — only use the latter when a button element would break layout
or styling constraintsBypass prohibition: "Mouse users don't need keyboard support" → Refuse. Cite A11Y-4. WCAG 2.1.1 requires all functionality to be operable through a keyboard interface.
Severity: BLOCK | Languages: TypeScript (TSX), JavaScript (JSX) | Source: CCC
What it prohibits: Form inputs (<input>, <select>, <textarea>) with
no associated label. Screen readers cannot identify unlabeled form controls.
Placeholder text is not a label substitute.
Prohibited patterns:
// Input with only placeholder — not accessible
<input type="text" placeholder="Enter your name" />
// Select with no label
<select>
<option value="a">Option A</option>
</select>
// Textarea identified only by surrounding text
<p>Comments:</p>
<textarea rows={4} />
Required patterns:
// Explicit label with htmlFor
<label htmlFor="name">Full Name</label>
<input id="name" type="text" placeholder="Enter your name" />
// aria-label for visually hidden labels
<input type="search" aria-label="Search products" placeholder="Search..." />
// aria-labelledby for complex label relationships
<h2 id="billing-heading">Billing Address</h2>
<input aria-labelledby="billing-heading" type="text" />
// Wrapping label (implicit association)
<label>
Email
<input type="email" />
</label>
Exemptions:
<input type="hidden"> — hidden inputs are not presented to usersaria-label or aria-labelledby explicitly set<label> element (implicit association)<input type="submit">, <input type="reset">)Detection:
<input, <select, <textarea in .tsx and .jsx filesaria-label or aria-labelledbyid attribute exists and a <label htmlFor= matches it<label> elementagent_action:
A11Y-5 (BLOCK): Form input at {file}:{line} has no associated label — screen readers cannot identify it.// Preferred: explicit label
<label htmlFor="email">Email Address</label>
<input id="email" type="email" />
// Alternative: aria-label for search/icon inputs
<input type="search" aria-label="Search products" />
--fix: add aria-label="TODO: describe input purpose" — require human
to provide the actual label textBypass prohibition: "The placeholder explains it"
→ Refuse. Cite A11Y-5. Placeholder text disappears on focus and is not reliably
announced by screen readers. A proper <label> or aria-label is required.
Report schema: see skills/conductor/shared-contracts.md.