Accessibility - RumenDamyanov/js-chess GitHub Wiki
Comprehensive guide to making the chess showcase accessible to all users, including those with disabilities.
This guide covers accessibility (a11y) implementation across all framework versions, ensuring:
- Screen reader compatibility
- Keyboard navigation
- ARIA labels and landmarks
- Color contrast compliance
- Focus management
- Cognitive accessibility
- Motor accessibility
The chess showcase follows WCAG 2.1 AA guidelines:
<!-- Semantic HTML structure -->
<main role="main" aria-label="Chess Game">
<section aria-label="Chess Board">
<div class="chess-board"
role="grid"
aria-label="8x8 chess board"
tabindex="0">
<!-- Board squares -->
</div>
</section>
<aside aria-label="Game Information">
<section aria-label="Move History">
<!-- Move list -->
</section>
</aside>
</main>
Provide comprehensive audio descriptions for chess positions:
<!-- Chess square with piece -->
<div class="chess-square"
role="gridcell"
aria-label="e4, white pawn"
data-square="e4"
tabindex="-1">
<span class="chess-piece" aria-hidden="true">♙</span>
</div>
<!-- Empty square -->
<div class="chess-square"
role="gridcell"
aria-label="e5, empty square"
data-square="e5"
tabindex="-1">
</div>
Full keyboard support for all interactions:
/* Keyboard focus indicators */
.chess-square:focus {
outline: 3px solid var(--color-focus);
outline-offset: 2px;
z-index: 10;
}
.chess-piece:focus {
outline: 2px solid var(--color-focus);
outline-offset: 1px;
}
/* High contrast focus for accessibility */
@media (prefers-contrast: high) {
.chess-square:focus {
outline: 4px solid #000000;
background-color: #ffff00;
}
}
// shared/accessibility/ScreenReaderAnnouncer.js
export class ScreenReaderAnnouncer {
constructor() {
this.announcer = this.createAnnouncer();
this.gameState = null;
this.lastMove = null;
}
createAnnouncer() {
const announcer = document.createElement('div');
announcer.setAttribute('aria-live', 'polite');
announcer.setAttribute('aria-atomic', 'true');
announcer.className = 'sr-only';
announcer.style.cssText = `
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
`;
document.body.appendChild(announcer);
return announcer;
}
announceMove(move) {
const announcement = this.formatMoveAnnouncement(move);
this.announce(announcement);
this.lastMove = move;
}
announceBoardState(gameState) {
if (gameState.inCheck) {
this.announce(`${gameState.currentPlayer} king is in check`);
}
if (gameState.gameStatus === 'checkmate') {
this.announce(`Checkmate! ${gameState.winner} wins the game`);
}
if (gameState.gameStatus === 'stalemate') {
this.announce('Stalemate! The game is a draw');
}
this.gameState = gameState;
}
announcePositionSummary() {
if (!this.gameState) return;
const summary = this.generatePositionSummary(this.gameState.board);
this.announce(`Board position: ${summary}`);
}
formatMoveAnnouncement(move) {
let announcement = '';
// Piece type
const pieceNames = {
'P': 'pawn', 'R': 'rook', 'N': 'knight',
'B': 'bishop', 'Q': 'queen', 'K': 'king'
};
const pieceName = pieceNames[move.piece.toUpperCase()] || 'piece';
const color = move.piece === move.piece.toUpperCase() ? 'white' : 'black';
// Special moves
if (move.castling) {
announcement = `${color} castles ${move.castling}`;
} else {
announcement = `${color} ${pieceName} moves from ${move.from} to ${move.to}`;
if (move.captured) {
const capturedPiece = pieceNames[move.captured.toUpperCase()] || 'piece';
announcement += `, capturing ${capturedPiece}`;
}
if (move.promotion) {
const promotedPiece = pieceNames[move.promotion] || 'piece';
announcement += `, promoting to ${promotedPiece}`;
}
if (move.enPassant) {
announcement += ', en passant capture';
}
}
return announcement;
}
generatePositionSummary(board) {
const pieces = { white: [], black: [] };
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = board[row][col];
if (piece) {
const color = piece === piece.toUpperCase() ? 'white' : 'black';
const type = piece.toUpperCase();
const square = String.fromCharCode(97 + col) + (8 - row);
pieces[color].push(`${type} on ${square}`);
}
}
}
let summary = '';
if (pieces.white.length > 0) {
summary += `White pieces: ${pieces.white.join(', ')}. `;
}
if (pieces.black.length > 0) {
summary += `Black pieces: ${pieces.black.join(', ')}.`;
}
return summary;
}
announce(message) {
this.announcer.textContent = message;
// Clear after announcement is read
setTimeout(() => {
this.announcer.textContent = '';
}, 1000);
}
announceGameStart() {
this.announce('New chess game started. White to move. Use arrow keys to navigate the board.');
}
announceSquareContents(square, piece) {
let announcement = `Square ${square}`;
if (piece) {
const pieceNames = {
'P': 'pawn', 'R': 'rook', 'N': 'knight',
'B': 'bishop', 'Q': 'queen', 'K': 'king'
};
const pieceName = pieceNames[piece.toUpperCase()] || 'piece';
const color = piece === piece.toUpperCase() ? 'white' : 'black';
announcement += `, ${color} ${pieceName}`;
} else {
announcement += ', empty';
}
this.announce(announcement);
}
}
// shared/accessibility/KeyboardNavigationManager.js
export class KeyboardNavigationManager {
constructor(boardElement, gameState, onSquareSelect, onMove) {
this.board = boardElement;
this.gameState = gameState;
this.onSquareSelect = onSquareSelect;
this.onMove = onMove;
this.currentSquare = null;
this.selectedSquare = null;
this.focusedSquare = 'e1'; // Start at white king position
this.setupKeyboardListeners();
this.updateSquareTabIndexes();
}
setupKeyboardListeners() {
this.board.addEventListener('keydown', this.handleKeyDown.bind(this));
this.board.addEventListener('focus', this.handleFocus.bind(this));
}
handleKeyDown(event) {
switch (event.key) {
case 'ArrowUp':
event.preventDefault();
this.moveUp();
break;
case 'ArrowDown':
event.preventDefault();
this.moveDown();
break;
case 'ArrowLeft':
event.preventDefault();
this.moveLeft();
break;
case 'ArrowRight':
event.preventDefault();
this.moveRight();
break;
case 'Enter':
case ' ':
event.preventDefault();
this.selectCurrentSquare();
break;
case 'Escape':
event.preventDefault();
this.clearSelection();
break;
case 'Home':
event.preventDefault();
this.goToSquare('a8');
break;
case 'End':
event.preventDefault();
this.goToSquare('h1');
break;
case 'Tab':
if (!event.shiftKey) {
this.handleTabNavigation(event);
}
break;
}
}
moveUp() {
const [file, rank] = this.parseSquare(this.focusedSquare);
if (rank < 8) {
this.goToSquare(file + (rank + 1));
}
}
moveDown() {
const [file, rank] = this.parseSquare(this.focusedSquare);
if (rank > 1) {
this.goToSquare(file + (rank - 1));
}
}
moveLeft() {
const [file, rank] = this.parseSquare(this.focusedSquare);
const fileIndex = file.charCodeAt(0) - 97;
if (fileIndex > 0) {
const newFile = String.fromCharCode(96 + fileIndex);
this.goToSquare(newFile + rank);
}
}
moveRight() {
const [file, rank] = this.parseSquare(this.focusedSquare);
const fileIndex = file.charCodeAt(0) - 97;
if (fileIndex < 7) {
const newFile = String.fromCharCode(98 + fileIndex);
this.goToSquare(newFile + rank);
}
}
goToSquare(square) {
this.focusedSquare = square;
const squareElement = this.getSquareElement(square);
if (squareElement) {
this.updateSquareTabIndexes();
squareElement.focus();
this.announceSquareContents(square);
}
}
selectCurrentSquare() {
if (this.selectedSquare === this.focusedSquare) {
// Deselect if clicking same square
this.clearSelection();
} else if (this.selectedSquare) {
// Make move
this.onMove(this.selectedSquare, this.focusedSquare);
this.clearSelection();
} else {
// Select square
this.selectedSquare = this.focusedSquare;
this.onSquareSelect(this.focusedSquare);
this.updateSquareStates();
}
}
clearSelection() {
this.selectedSquare = null;
this.onSquareSelect(null);
this.updateSquareStates();
}
updateSquareTabIndexes() {
const squares = this.board.querySelectorAll('.chess-square');
squares.forEach(square => {
const squareId = square.dataset.square;
square.setAttribute('tabindex', squareId === this.focusedSquare ? '0' : '-1');
});
}
updateSquareStates() {
const squares = this.board.querySelectorAll('.chess-square');
squares.forEach(square => {
const squareId = square.dataset.square;
if (squareId === this.selectedSquare) {
square.setAttribute('aria-selected', 'true');
square.classList.add('selected');
} else {
square.setAttribute('aria-selected', 'false');
square.classList.remove('selected');
}
});
}
handleFocus() {
// Announce board state when board gains focus
this.announceSquareContents(this.focusedSquare);
}
handleTabNavigation(event) {
// Custom tab navigation within the chess board
const squares = this.board.querySelectorAll('.chess-square');
const currentIndex = Array.from(squares).findIndex(
square => square.dataset.square === this.focusedSquare
);
if (currentIndex < squares.length - 1) {
event.preventDefault();
const nextSquare = squares[currentIndex + 1];
this.goToSquare(nextSquare.dataset.square);
}
}
parseSquare(square) {
return [square[0], parseInt(square[1])];
}
getSquareElement(square) {
return this.board.querySelector(`[data-square="${square}"]`);
}
announceSquareContents(square) {
const piece = this.gameState.board[this.getSquareCoords(square).row][this.getSquareCoords(square).col];
// This would use the ScreenReaderAnnouncer
if (window.screenReaderAnnouncer) {
window.screenReaderAnnouncer.announceSquareContents(square, piece);
}
}
getSquareCoords(square) {
const file = square.charCodeAt(0) - 97;
const rank = parseInt(square[1]) - 1;
return { row: 8 - parseInt(square[1]), col: file };
}
}
// shared/accessibility/ARIAManager.js
export class ARIAManager {
constructor() {
this.setupStaticARIA();
}
setupStaticARIA() {
// Main landmarks
const main = document.querySelector('main');
if (main) {
main.setAttribute('role', 'main');
main.setAttribute('aria-label', 'Chess Game Application');
}
// Chess board
const board = document.querySelector('.chess-board');
if (board) {
board.setAttribute('role', 'grid');
board.setAttribute('aria-label', '8 by 8 chess board');
board.setAttribute('aria-rowcount', '8');
board.setAttribute('aria-colcount', '8');
}
// Game controls
const controls = document.querySelector('.game-controls');
if (controls) {
controls.setAttribute('role', 'toolbar');
controls.setAttribute('aria-label', 'Game controls');
}
}
updateSquareARIA(square, piece, row, col, isSelected, isValidMove, isInCheck) {
square.setAttribute('role', 'gridcell');
square.setAttribute('aria-rowindex', 8 - row + 1);
square.setAttribute('aria-colindex', col + 1);
// Build descriptive label
const file = String.fromCharCode(97 + col);
const rank = 8 - row;
const squareId = file + rank;
let label = `${squareId}`;
if (piece) {
const pieceNames = {
'P': 'pawn', 'R': 'rook', 'N': 'knight',
'B': 'bishop', 'Q': 'queen', 'K': 'king'
};
const pieceName = pieceNames[piece.toUpperCase()] || 'piece';
const color = piece === piece.toUpperCase() ? 'white' : 'black';
label += `, ${color} ${pieceName}`;
} else {
label += ', empty square';
}
if (isSelected) {
label += ', selected';
}
if (isValidMove) {
label += ', valid move';
}
if (isInCheck) {
label += ', in check';
}
square.setAttribute('aria-label', label);
square.setAttribute('aria-selected', isSelected ? 'true' : 'false');
// Additional states
if (isValidMove) {
square.setAttribute('aria-describedby', 'valid-move-description');
}
if (isInCheck) {
square.setAttribute('aria-live', 'assertive');
}
}
updateGameStatus(status, currentPlayer, winner) {
const statusElement = document.getElementById('game-status') || this.createStatusElement();
let statusText = '';
switch (status) {
case 'active':
statusText = `${currentPlayer}'s turn to move`;
break;
case 'check':
statusText = `${currentPlayer} is in check`;
break;
case 'checkmate':
statusText = `Checkmate! ${winner} wins`;
break;
case 'stalemate':
statusText = 'Stalemate - game is a draw';
break;
case 'draw':
statusText = 'Game ended in a draw';
break;
}
statusElement.textContent = statusText;
statusElement.setAttribute('aria-live', status === 'check' ? 'assertive' : 'polite');
}
createStatusElement() {
const status = document.createElement('div');
status.id = 'game-status';
status.className = 'sr-only';
status.setAttribute('aria-live', 'polite');
status.setAttribute('aria-atomic', 'true');
document.body.appendChild(status);
return status;
}
updateMoveHistory(moves, currentMoveIndex) {
const historyContainer = document.querySelector('.move-history');
if (!historyContainer) return;
historyContainer.setAttribute('role', 'log');
historyContainer.setAttribute('aria-label', 'Move history');
historyContainer.setAttribute('aria-live', 'polite');
const moveButtons = historyContainer.querySelectorAll('.move-button');
moveButtons.forEach((button, index) => {
const isCurrentMove = index === currentMoveIndex;
button.setAttribute('aria-current', isCurrentMove ? 'true' : 'false');
if (isCurrentMove) {
button.setAttribute('aria-label', `Current move: ${button.textContent}`);
}
});
}
announceCapture(capturedPiece, captureSquare) {
const announcement = document.createElement('div');
announcement.className = 'sr-only';
announcement.setAttribute('aria-live', 'assertive');
announcement.textContent = `${capturedPiece} captured on ${captureSquare}`;
document.body.appendChild(announcement);
setTimeout(() => {
document.body.removeChild(announcement);
}, 2000);
}
}
// angular-chess/src/app/directives/accessibility.directive.ts
import { Directive, ElementRef, Input, OnInit, OnDestroy } from '@angular/core';
import { KeyboardNavigationManager } from '../../../shared/accessibility/KeyboardNavigationManager';
import { ARIAManager } from '../../../shared/accessibility/ARIAManager';
@Directive({
selector: '[appAccessibility]'
})
export class AccessibilityDirective implements OnInit, OnDestroy {
@Input() gameState: any;
@Input() onSquareSelect: (square: string) => void = () => {};
@Input() onMove: (from: string, to: string) => void = () => {};
private keyboardManager?: KeyboardNavigationManager;
private ariaManager: ARIAManager;
constructor(private el: ElementRef) {
this.ariaManager = new ARIAManager();
}
ngOnInit() {
if (this.el.nativeElement.classList.contains('chess-board')) {
this.keyboardManager = new KeyboardNavigationManager(
this.el.nativeElement,
this.gameState,
this.onSquareSelect,
this.onMove
);
}
this.setupAccessibility();
}
ngOnDestroy() {
// Cleanup if needed
}
private setupAccessibility() {
const element = this.el.nativeElement;
// Add skip link for keyboard users
this.addSkipLink();
// Ensure proper heading structure
this.enforceHeadingStructure();
// Add high contrast mode detection
this.setupHighContrastMode();
}
private addSkipLink() {
const skipLink = document.createElement('a');
skipLink.href = '#chess-board';
skipLink.textContent = 'Skip to chess board';
skipLink.className = 'skip-link';
skipLink.style.cssText = `
position: absolute;
top: -40px;
left: 6px;
background: #000;
color: #fff;
padding: 8px;
text-decoration: none;
z-index: 1000;
`;
skipLink.addEventListener('focus', () => {
skipLink.style.top = '6px';
});
skipLink.addEventListener('blur', () => {
skipLink.style.top = '-40px';
});
document.body.insertBefore(skipLink, document.body.firstChild);
}
private enforceHeadingStructure() {
// Ensure proper heading hierarchy
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
let expectedLevel = 1;
headings.forEach(heading => {
const currentLevel = parseInt(heading.tagName[1]);
if (currentLevel > expectedLevel + 1) {
console.warn(`Heading level ${currentLevel} skips level ${expectedLevel + 1}`);
}
expectedLevel = currentLevel;
});
}
private setupHighContrastMode() {
const supportsHighContrast = window.matchMedia('(prefers-contrast: high)');
const updateHighContrast = (matches: boolean) => {
document.body.classList.toggle('high-contrast', matches);
};
updateHighContrast(supportsHighContrast.matches);
supportsHighContrast.addEventListener('change', (e) => updateHighContrast(e.matches));
}
}
// react-chess/src/hooks/useAccessibility.js
import { useEffect, useRef, useCallback } from 'react';
import { KeyboardNavigationManager } from '../../shared/accessibility/KeyboardNavigationManager';
import { ScreenReaderAnnouncer } from '../../shared/accessibility/ScreenReaderAnnouncer';
import { ARIAManager } from '../../shared/accessibility/ARIAManager';
export function useAccessibility(gameState, onSquareSelect, onMove) {
const boardRef = useRef(null);
const keyboardManagerRef = useRef(null);
const announcerRef = useRef(null);
const ariaManagerRef = useRef(null);
useEffect(() => {
if (!announcerRef.current) {
announcerRef.current = new ScreenReaderAnnouncer();
}
if (!ariaManagerRef.current) {
ariaManagerRef.current = new ARIAManager();
}
return () => {
// Cleanup if needed
};
}, []);
useEffect(() => {
if (boardRef.current && gameState) {
if (!keyboardManagerRef.current) {
keyboardManagerRef.current = new KeyboardNavigationManager(
boardRef.current,
gameState,
onSquareSelect,
onMove
);
}
}
}, [gameState, onSquareSelect, onMove]);
const announceMove = useCallback((move) => {
if (announcerRef.current) {
announcerRef.current.announceMove(move);
}
}, []);
const announceGameState = useCallback((state) => {
if (announcerRef.current) {
announcerRef.current.announceBoardState(state);
}
if (ariaManagerRef.current) {
ariaManagerRef.current.updateGameStatus(
state.gameStatus,
state.currentPlayer,
state.winner
);
}
}, []);
const announceCheck = useCallback((color) => {
if (announcerRef.current) {
announcerRef.current.announce(`${color} king is in check`);
}
}, []);
const announceGameEnd = useCallback((result, winner) => {
if (announcerRef.current) {
let message = '';
if (result === 'checkmate') {
message = `Checkmate! ${winner} wins the game.`;
} else if (result === 'stalemate') {
message = 'Stalemate! The game is a draw.';
} else if (result === 'draw') {
message = 'The game ended in a draw.';
}
announcerRef.current.announce(message);
}
}, []);
return {
boardRef,
announceMove,
announceGameState,
announceCheck,
announceGameEnd
};
}
// vue-chess/src/composables/useAccessibility.js
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { KeyboardNavigationManager } from '../../shared/accessibility/KeyboardNavigationManager';
import { ScreenReaderAnnouncer } from '../../shared/accessibility/ScreenReaderAnnouncer';
import { ARIAManager } from '../../shared/accessibility/ARIAManager';
export function useAccessibility(gameState, onSquareSelect, onMove) {
const boardRef = ref(null);
let keyboardManager = null;
let announcer = null;
let ariaManager = null;
onMounted(() => {
announcer = new ScreenReaderAnnouncer();
ariaManager = new ARIAManager();
if (boardRef.value) {
keyboardManager = new KeyboardNavigationManager(
boardRef.value,
gameState.value,
onSquareSelect,
onMove
);
}
});
onUnmounted(() => {
// Cleanup if needed
});
watch(gameState, (newState) => {
if (announcer) {
announcer.announceBoardState(newState);
}
if (ariaManager) {
ariaManager.updateGameStatus(
newState.gameStatus,
newState.currentPlayer,
newState.winner
);
}
}, { deep: true });
const announceMove = (move) => {
if (announcer) {
announcer.announceMove(move);
}
};
const announceGameStart = () => {
if (announcer) {
announcer.announceGameStart();
}
};
return {
boardRef,
announceMove,
announceGameStart
};
}
/* Ensure WCAG AA contrast ratios */
:root {
/* High contrast color palette */
--color-text-primary: #000000;
--color-text-secondary: #333333;
--color-background: #ffffff;
--color-surface: #f8f9fa;
/* Chess-specific colors with high contrast */
--chess-light-square: #f0d9b5;
--chess-dark-square: #b58863;
--chess-selected: #646cff;
--chess-valid-move: #22c55e;
--chess-capture: #ef4444;
--chess-check: #f59e0b;
/* Focus indicator */
--color-focus: #0066cc;
--color-focus-visible: #0052a3;
}
/* High contrast mode */
@media (prefers-contrast: high) {
:root {
--color-text-primary: #000000;
--color-background: #ffffff;
--chess-light-square: #ffffff;
--chess-dark-square: #000000;
--chess-selected: #ff0000;
--chess-valid-move: #00ff00;
--chess-capture: #ff0000;
--color-focus: #ff0000;
}
.chess-piece--white {
color: #000000;
text-shadow: 2px 2px 0 #ffffff;
}
.chess-piece--black {
color: #ffffff;
text-shadow: 2px 2px 0 #000000;
}
}
/* Reduced motion preferences */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Color blindness accommodations */
.colorblind-mode {
--chess-valid-move: #0066cc;
--chess-capture: #cc6600;
--chess-check: #9900cc;
}
/* Pattern alternatives for color-only information */
.chess-square--valid-move::before {
content: '';
position: absolute;
top: 2px;
right: 2px;
width: 8px;
height: 8px;
background: repeating-linear-gradient(
45deg,
var(--chess-valid-move),
var(--chess-valid-move) 2px,
transparent 2px,
transparent 4px
);
}
.chess-square--capture::before {
content: '';
position: absolute;
inset: 4px;
border: 3px dashed var(--chess-capture);
border-radius: 50%;
}
// shared/testing/AccessibilityTestUtils.js
import { axe } from 'jest-axe';
export class AccessibilityTestUtils {
static async runAxeTests(element) {
const results = await axe(element);
return results;
}
static async testKeyboardNavigation(element) {
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const results = {
totalElements: focusableElements.length,
hasTabIndex: 0,
hasProperFocus: 0
};
focusableElements.forEach(el => {
if (el.tabIndex >= 0) {
results.hasTabIndex++;
}
// Test focus visibility
el.focus();
const computedStyle = window.getComputedStyle(el);
if (computedStyle.outline !== 'none' || computedStyle.boxShadow !== 'none') {
results.hasProperFocus++;
}
});
return results;
}
static testScreenReaderContent(element) {
const ariaLabels = element.querySelectorAll('[aria-label]');
const altTexts = element.querySelectorAll('img[alt]');
const headings = element.querySelectorAll('h1, h2, h3, h4, h5, h6');
return {
ariaLabelsCount: ariaLabels.length,
altTextsCount: altTexts.length,
headingsCount: headings.length,
hasMainLandmark: !!element.querySelector('[role="main"], main'),
hasSkipLink: !!element.querySelector('.skip-link')
};
}
static checkColorContrast(backgroundColor, textColor) {
// Simple contrast ratio calculation
const getLuminance = (color) => {
const rgb = parseInt(color.slice(1), 16);
const r = (rgb >> 16) & 0xff;
const g = (rgb >> 8) & 0xff;
const b = (rgb >> 0) & 0xff;
const [rs, gs, bs] = [r, g, b].map(c => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
};
const l1 = getLuminance(backgroundColor);
const l2 = getLuminance(textColor);
const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
return {
ratio,
passesAA: ratio >= 4.5,
passesAAA: ratio >= 7
};
}
}
## Accessibility Testing Checklist
### Keyboard Navigation
- [ ] All interactive elements are keyboard accessible
- [ ] Tab order is logical and intuitive
- [ ] Focus indicators are clearly visible
- [ ] No keyboard traps exist
- [ ] Escape key works to cancel actions
### Screen Reader Testing
- [ ] All content is read by screen readers
- [ ] Board state is announced clearly
- [ ] Moves are announced with sufficient detail
- [ ] Game status changes are announced
- [ ] Navigation instructions are provided
### Visual Accessibility
- [ ] Color contrast meets WCAG AA standards
- [ ] Information isn't conveyed by color alone
- [ ] Text is readable at 200% zoom
- [ ] Layout works in high contrast mode
- [ ] Content is readable with CSS disabled
### Motor Accessibility
- [ ] Touch targets are at least 44x44 pixels
- [ ] Click/tap areas don't overlap
- [ ] Drag and drop has keyboard alternatives
- [ ] No actions require precise timing
- [ ] Alternatives exist for complex gestures
### Cognitive Accessibility
- [ ] Instructions are clear and simple
- [ ] Important information is repeated
- [ ] Consistent navigation and layout
- [ ] Error messages are descriptive
- [ ] Users can pause/stop animations
// shared/accessibility/AccessibilitySettings.js
export class AccessibilitySettings {
constructor() {
this.settings = this.loadSettings();
this.applySettings();
}
loadSettings() {
const defaults = {
reduceMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
highContrast: window.matchMedia('(prefers-contrast: high)').matches,
screenReaderMode: false,
soundEnabled: true,
largeText: false,
colorBlindMode: false,
keyboardOnlyMode: false
};
const stored = localStorage.getItem('chess-accessibility-settings');
return stored ? { ...defaults, ...JSON.parse(stored) } : defaults;
}
saveSettings() {
localStorage.setItem('chess-accessibility-settings', JSON.stringify(this.settings));
this.applySettings();
}
applySettings() {
document.body.classList.toggle('reduce-motion', this.settings.reduceMotion);
document.body.classList.toggle('high-contrast', this.settings.highContrast);
document.body.classList.toggle('screen-reader-mode', this.settings.screenReaderMode);
document.body.classList.toggle('large-text', this.settings.largeText);
document.body.classList.toggle('colorblind-mode', this.settings.colorBlindMode);
document.body.classList.toggle('keyboard-only', this.settings.keyboardOnlyMode);
}
updateSetting(key, value) {
this.settings[key] = value;
this.saveSettings();
}
getSetting(key) {
return this.settings[key];
}
resetToDefaults() {
this.settings = this.loadSettings();
localStorage.removeItem('chess-accessibility-settings');
this.applySettings();
}
}
- Theming - Customizable themes and color schemes
- Performance - Optimizing for assistive technologies
- Testing Guide - Comprehensive accessibility testing
- Responsive Design - Mobile accessibility considerations