Reviews educational assessment interfaces for WCAG 2.2 Level AA compliance and assessment-specific accessibility needs. Use when implementing new interactions, reviewing UI changes, or conducting accessibility audits of test-taking interfaces.
This skill helps review educational assessment component libraries for accessibility compliance, with special focus on K-12 learners, web components/custom elements, and reusable UI interactions distributed as NPM packages.
Invoke this skill when:
Educational assessment components have unique accessibility requirements beyond standard web content:
Example Issues to Catch:
<!-- ❌ Missing alt text on question image -->
<img src="diagram.png" />
<!-- ❌ Alt text gives away answer -->
<img src="triangle.png" alt="equilateral triangle" />
<!-- ✅ Descriptive without revealing answer -->
<img src="triangle.png" alt="A triangle with three sides and three angles" />
<!-- ✅ Decorative image properly marked -->
<img src="decorative-border.png" alt="" role="presentation" />
Example Issues to Catch:
<!-- ❌ Relies on visual position -->
<p>Select the answer in the top-right box</p>
<!-- ✅ Position-independent instructions -->
<p>Select the answer labeled "B"</p>
<!-- ❌ Structure not programmatic -->
<div class="heading">Question 1</div>
<!-- ✅ Semantic structure -->
<h2>Question 1</h2>
<!-- ❌ Instructions rely on color -->
<p>Click the green button to continue</p>
<!-- ✅ Color-independent -->
<p>Click the "Continue" button (highlighted in green) to proceed</p>
Example Issues to Catch:
/* ❌ Insufficient contrast: 2.8:1 */
.choice-option {
color: #999; /* gray */
background: #fff; /* white */
}
/* ✅ Sufficient contrast: 4.6:1 */
.choice-option {
color: #595959; /* darker gray */
background: #fff;
}
/* ❌ Touch target too small: 30×30px */
.drag-handle {
width: 30px;
height: 30px;
}
/* ✅ WCAG 2.2 minimum: 44×44px */
.drag-handle {
width: 44px;
height: 44px;
}
Educational interaction keyboard patterns:
Example Issues to Catch:
<!-- ❌ Click handler without keyboard support -->
<div on:click={selectChoice}>Choice A</div>
<!-- ✅ Keyboard accessible -->
<button on:click={selectChoice}>Choice A</button>
<!-- ✅ Custom keyboard support for drag-and-drop -->
<div
role="button"
tabindex="0"
on:keydown={(e) => {
if (e.key === ' ' || e.key === 'Enter') {
toggleGrab();
}
}}
>
Draggable item
</div>
Example Issues to Catch:
<!-- ❌ Hard time limit without extension -->
<Timer duration={60} onExpire={submitAssessment} />
<!-- ✅ Configurable time with extensions -->
<Timer
duration={timeLimit}
allowExtensions={true}
extensionMultiplier={1.5}
maxExtensions={2}
onExpire={handleTimeUp}
/>
Example Issues to Catch:
<!-- ❌ Missing skip link -->
<nav><!-- Navigation --></nav>
<main><!-- Assessment content --></main>
<!-- ✅ Skip link present -->
<a href="#main-content" class="skip-link">Skip to assessment</a>
<nav><!-- Navigation --></nav>
<main id="main-content"><!-- Assessment content --></main>
<!-- ❌ Focus indicator removed -->
<style>
button:focus { outline: none; }
</style>
<!-- ✅ Clear focus indicator -->
<style>
button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
</style>
Example Issues to Catch:
<!-- ❌ Touch target too small: 20×20px -->
<button style="width: 20px; height: 20px;">×</button>
<!-- ✅ WCAG 2.2 compliant: 44×44px -->
<button style="width: 44px; height: 44px;" aria-label="Close">×</button>
<!-- ❌ Drag-only interaction -->
<div draggable="true" on:drag={handleDrag}>
Drag to reorder
</div>
<!-- ✅ Keyboard alternative provided -->
<div
draggable="true"
role="button"
tabindex="0"
on:drag={handleDrag}
on:keydown={handleKeyboardReorder}
>
Drag to reorder or use arrow keys
</div>
Example Issues to Catch:
<!-- ❌ Missing language attribute -->
<html>
<!-- ✅ Language specified -->
<html lang="en">
<!-- ❌ Foreign language not marked -->
<p>The French phrase "bonjour" means hello.</p>
<!-- ✅ Language change marked -->
<p>The French phrase <span lang="fr">bonjour</span> means hello.</p>
Example Issues to Catch:
<!-- ❌ Focus triggers navigation -->
<button on:focus={goToNextQuestion}>Next</button>
<!-- ✅ Explicit action required -->
<button on:click={goToNextQuestion}>Next</button>
<!-- ❌ Inconsistent button labels -->
<!-- On item 1: --><button>Continue</button>
<!-- On item 2: --><button>Next</button>
<!-- On item 3: --><button>Proceed</button>
<!-- ✅ Consistent labeling -->
<button>Next Question</button>
Example Issues to Catch:
<!-- ❌ Vague error message -->
<div role="alert">Invalid input</div>
<!-- ✅ Specific, actionable error -->
<div role="alert">
Please enter a number between 1 and 100. You entered "abc".
</div>
<!-- ❌ Submit without confirmation -->
<button on:click={submitAssessment}>Submit Test</button>
<!-- ✅ Confirmation before irreversible action -->
<button on:click={confirmSubmit}>Submit Test</button>
{#if showConfirmation}
<dialog>
<p>Are you sure you want to submit? You cannot change answers after submitting.</p>
<button on:click={submitAssessment}>Yes, Submit</button>
<button on:click={cancelSubmit}>No, Go Back</button>
</dialog>
{/if}
Example Issues to Catch:
<!-- ❌ Custom component without ARIA -->
<div class="checkbox" on:click={toggle}>
{#if checked}✓{/if}
</div>
<!-- ✅ Proper ARIA attributes -->
<div
role="checkbox"
aria-checked={checked}
tabindex="0"
on:click={toggle}
on:keydown={handleKey}
>
{#if checked}✓{/if}
</div>
<!-- ❌ Status update not announced -->
<div>Answer saved</div>
<!-- ✅ Status announced to screen readers -->
<div role="status" aria-live="polite">
Answer saved
</div>
// ✅ Accessible custom element with proper ARIA
class ChoiceInteraction extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open', delegatesFocus: true });
}
connectedCallback() {
this.setAttribute('role', 'radiogroup');
this.setAttribute('aria-label', this.getAttribute('label') || 'Select an answer');
this.render();
}
}
<!-- ✅ Accessible radio/checkbox group -->
<fieldset>
<legend>Which of the following is a prime number?</legend>
{#each choices as choice, i}
<label>
<input
type="radio"
name="question-1"
value={choice.id}
checked={selected === choice.id}
/>
{choice.text}
</label>
{/each}
</fieldset>
<!-- ✅ Keyboard-accessible drag-and-drop -->
<div
role="button"
tabindex="0"
aria-grabbed={isGrabbed}
aria-label="Draggable item: {label}. Press space to pick up."
on:keydown={(e) => {
if (e.key === ' ') {
e.preventDefault();
toggleGrab();
} else if (isGrabbed && e.key.startsWith('Arrow')) {
e.preventDefault();
moveWithKeyboard(e.key);
}
}}
>
{label}
</div>
<!-- Screen reader feedback -->
<div role="status" aria-live="assertive" aria-atomic="true">
{#if isGrabbed}
{label} picked up. Use arrow keys to move, space to drop.
{:else if justDropped}
{label} dropped.
{/if}
</div>
<!-- ✅ Accessible math with MathML and fallback -->
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
<mrow>
<mi>x</mi>
<mo>=</mo>
<mfrac>
<mrow><mo>−</mo><mi>b</mi></mrow>
<mrow><mn>2</mn><mi>a</mi></mrow>
</mfrac>
</mrow>
</math>
<span class="sr-only">
x equals negative b divided by 2a
</span>
<!-- Or with aria-label -->
<div class="math-formula" aria-label="x equals negative b over 2a">
[rendered LaTeX]
</div>
<!-- ✅ Accessible time warnings -->
<div role="timer" aria-live="polite" aria-atomic="true">
{#if minutesRemaining <= 5}
<span aria-label="Warning: {minutesRemaining} minutes remaining">
⚠️ {minutesRemaining}:00
</span>
{:else}
{minutesRemaining}:00
{/if}
</div>
<!-- Additional aria-live region for critical warnings -->
{#if minutesRemaining === 1}
<div role="alert" aria-live="assertive">
Warning: Only 1 minute remaining
</div>
{/if}
When conducting an accessibility review:
Keyboard Navigation Test
Screen Reader Test
Color/Contrast Check
Touch Target Audit
Code Review
Automated Testing
Provide feedback in this structure:
For each violation:
# Run Playwright accessibility tests for example app
bun --filter @pie-elements/example test:e2e
# Run specific a11y test suite
bun --filter @pie-elements/example test:e2e a11y
# Check for ARIA violations with axe-core (if configured)
npm run test:a11y
# Test component in isolation
bun test -- --grep "accessibility"