Accessibility Guidelines: 40 Days of Existential Threats

WCAG 2.1 Level AA Compliance for Interactive Experience Collection


1. WCAG Compliance Overview

Target Compliance: WCAG 2.1 Level AA

The β€œ40 Days of Existential Threats” interactive experience collection must meet WCAG 2.1 Level AA standards to ensure accessibility for users with diverse abilities.

The Four POUR Principles

PrincipleDescriptionWCAG Criteria
PerceivableInformation must be presentable in ways users can perceive1.1.1 - 1.4.13
OperableInterface components must be operable by all users2.1.1 - 2.5.6
UnderstandableInformation and UI operation must be understandable3.1.1 - 3.3.6
RobustContent must work with current and future assistive technologies4.1.1 - 4.1.3

Compliance Checklist Summary

β–‘ All images have appropriate alt text (1.1.1)
β–‘ Color is not the only means of conveying information (1.4.1)
β–‘ Audio content has transcripts/captions (1.2.1-1.2.4)
β–‘ Text contrast meets 4.5:1 minimum (1.4.3)
β–‘ All functionality available via keyboard (2.1.1)
β–‘ No keyboard traps (2.1.2)
β–‘ Focus order is logical (2.4.3)
β–‘ Focus is visible (2.4.7)
β–‘ Page has descriptive title (2.4.2)
β–‘ Form labels are associated (1.3.1, 3.3.2)
β–‘ Error messages are descriptive (3.3.1, 3.3.3)
β–‘ Animations respect prefers-reduced-motion (2.3.3)
β–‘ Touch targets are 44x44px minimum (2.5.5)

2. Color & Contrast

Minimum Contrast Ratios (WCAG 1.4.3, 1.4.11)

Element TypeMinimum RatioEnhanced (AAA)
Normal text (<18pt / <14pt bold)4.5:17:1
Large text (β‰₯18pt / β‰₯14pt bold)3:14.5:1
UI Components & Graphics3:1N/A
Focus indicators3:1N/A

Dark Theme Specific Considerations

For the dark, luminous aesthetic with bioluminescent accents:

/* Base dark theme with accessible contrast */
:root {
  /* Background colors - ensure 4.5:1 with text */
  --bg-primary: #0a0a0f;      /* Deepest black-blue */
  --bg-secondary: #12121a;    /* Slightly elevated */
  --bg-tertiary: #1a1a25;     /* Cards/surfaces */
  
  /* Text colors - must contrast 4.5:1 with backgrounds */
  --text-primary: #f0f0f5;    /* Primary text */
  --text-secondary: #a0a0b0;  /* Secondary text (min 4.5:1) */
  --text-muted: #707080;      /* Disabled/hints (min 4.5:1) */
  
  /* Bioluminescent accents - ensure contrast */
  --accent-cyan: #00d4aa;     /* Primary accent */
  --accent-purple: #a855f7;   /* Secondary accent */
  --accent-amber: #f59e0b;    /* Warning accent */
}

Bioluminescent Accent Color Contrast Requirements

/* Accent colors must meet 3:1 against backgrounds for UI elements */
.bioluminescent-button {
  background: transparent;
  border: 2px solid var(--accent-cyan);
  color: var(--accent-cyan);
  /* Border + text must have 3:1 contrast against bg */
}
 
/* For text on accent backgrounds, use dark overlay */
.accent-bg-text {
  background: var(--accent-cyan);
  color: var(--bg-primary); /* Dark text on light accent */
}

Danger/Warning/Success Color Contrast

/* Status colors with guaranteed contrast */
:root {
  --status-error: #ef4444;    /* Must be 3:1 against bg */
  --status-warning: #f59e0b;  /* Must be 3:1 against bg */
  --status-success: #22c55e;  /* Must be 3:1 against bg */
  --status-info: #3b82f6;     /* Must be 3:1 against bg */
}
 
/* Always pair with icons/text, never color alone */
.status-message {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.status-message::before {
  content: "⚠"; /* Icon + color */
}

Text on Glass-Morphism Backgrounds

/* Glass morphism with accessible text */
.glass-card {
  background: rgba(255, 255, 255, 0.05);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.1);
}
 
/* Ensure text has sufficient contrast */
.glass-card p {
  color: var(--text-primary); /* #f0f0f5 on dark bg = sufficient contrast */
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); /* Enhances readability */
}

Testing Tools

ToolPurposeURL
WebAIM Contrast CheckerManual color testingwebaim.org/resources/contrastchecker/
axe DevToolsAutomated contrast testingdeque.com/axe/devtools/
Stark (Figma/Sketch)Design phase testinggetstark.co
Colour Contrast AnalyserDesktop apptpgi.com/cca/
LighthouseAutomated auditBuilt into Chrome DevTools

Don’t Rely on Color Alone (WCAG 1.4.1)

<!-- ❌ INCORRECT: Color only -->
<span class="red-text">Critical threat level</span>
 
<!-- βœ… CORRECT: Color + icon + text -->
<span class="status-critical">
  <svg aria-hidden="true" class="icon-critical">...</svg>
  <span class="visually-hidden">Critical:</span>
  Threat level maximum
</span>
/* Visually hidden but screen reader accessible */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

3. Keyboard Navigation

All Interactive Elements Must Be Keyboard Accessible (WCAG 2.1.1)

<!-- βœ… Native elements are keyboard accessible by default -->
<button>Activate</button>
<a href="/path">Navigate</a>
<input type="text" />
<select>...</select>
 
<!-- ❌ Custom elements need tabindex and keyboard handlers -->
<div role="button" tabindex="0" 
     aria-pressed="false"
     onkeydown="handleKey(event)">
  Custom Button
</div>

Tab Order Logic (WCAG 2.4.3)

<!-- Tab order follows DOM order - keep it logical -->
<main>
  <h1>Experience Title</h1>
  <p>Description...</p>
  <button>Start Experience</button> <!-- Tab stop 1 -->
  <button>View Instructions</button> <!-- Tab stop 2 -->
  <div role="group" aria-label="Difficulty">
    <button aria-pressed="true">Easy</button> <!-- Tab stop 3 -->
    <button aria-pressed="false">Hard</button> <!-- Tab stop 4 -->
  </div>
</main>

Focus Visible Requirements (WCAG 2.4.7)

Minimum 2px outline with 3:1 contrast ratio:

/* Focus indicator consistent with dark theme */
:focus-visible {
  outline: 2px solid var(--accent-cyan);
  outline-offset: 2px;
  border-radius: 2px;
}
 
/* Enhanced focus for important elements */
button:focus-visible,
a:focus-visible {
  outline: 3px solid var(--accent-cyan);
  outline-offset: 3px;
  box-shadow: 0 0 0 4px rgba(0, 212, 170, 0.2);
}
 
/* Remove default focus, keep visible focus */
:focus {
  outline: none;
}
:focus-visible {
  outline: 2px solid var(--accent-cyan);
}
<!-- Skip link as first focusable element -->
<a href="#main-content" class="skip-link">
  Skip to main content
</a>
 
<nav aria-label="Main">...</nav>
<main id="main-content" tabindex="-1">
  <!-- Experience content -->
</main>
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: var(--accent-cyan);
  color: var(--bg-primary);
  padding: 8px 16px;
  z-index: 100;
  transition: top 0.2s;
}
 
.skip-link:focus {
  top: 0;
}

Keyboard Shortcuts Documentation

<!-- Keyboard shortcuts help modal -->
<button aria-haspopup="dialog" 
        aria-controls="keyboard-help"
        aria-label="Keyboard shortcuts">
  <svg aria-hidden="true">...</svg>
</button>
 
<dialog id="keyboard-help" aria-labelledby="keyboard-title">
  <h2 id="keyboard-title">Keyboard Shortcuts</h2>
  <dl>
    <dt><kbd>Tab</kbd></dt>
    <dd>Move to next interactive element</dd>
    <dt><kbd>Shift + Tab</kbd></dt>
    <dd>Move to previous interactive element</dd>
    <dt><kbd>Enter</kbd> / <kbd>Space</kbd></dt>
    <dd>Activate button or link</dd>
    <dt><kbd>Escape</kbd></dt>
    <dd>Close modal or pause experience</dd>
    <dt><kbd>Arrow Keys</kbd></dt>
    <dd>Navigate within components</dd>
  </dl>
  <button>Close</button>
</dialog>

Escape Key Behaviors

// Consistent Escape key behavior
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    // Priority: Close modals first
    const openModal = document.querySelector('[role="dialog"][open]');
    if (openModal) {
      closeModal(openModal);
      return;
    }
    
    // Then pause experiences
    const activeExperience = document.querySelector('[data-experience-active]');
    if (activeExperience) {
      pauseExperience(activeExperience);
      return;
    }
    
    // Finally, exit full-screen
    if (document.fullscreenElement) {
      document.exitFullscreen();
    }
  }
});

Enter/Space Activation

// Keyboard handler for custom interactive elements
function handleKey(event) {
  const isEnter = event.key === 'Enter';
  const isSpace = event.key === ' ';
  
  if (isEnter || isSpace) {
    event.preventDefault();
    
    // Space on buttons should not scroll
    if (isSpace && event.target.tagName === 'BUTTON') {
      event.preventDefault();
    }
    
    // Activate the element
    activateElement(event.target);
  }
}

4. Screen Reader Support

Semantic HTML Requirements (WCAG 1.3.1)

<!-- βœ… Use semantic elements -->
<header>
  <nav aria-label="Main navigation">
    <ul>
      <li><a href="/">Home</a></li>
      <li><a href="/experiences">Experiences</a></li>
    </ul>
  </nav>
</header>
 
<main>
  <article>
    <header>
      <h1>Day 1: The First Threat</h1>
      <time datetime="2024-01-01">January 1, 2024</time>
    </header>
    <section aria-labelledby="overview-heading">
      <h2 id="overview-heading">Overview</h2>
      <p>...</p>
    </section>
  </article>
</main>
 
<footer>
  <p>&copy; 2024 40 Days of Existential Threats</p>
</footer>

Heading Hierarchy (WCAG 1.3.1)

<!-- βœ… Proper heading hierarchy -->
<h1>40 Days of Existential Threats</h1>
  <h2>Day 1: The Awakening</h2>
    <h3>Introduction</h3>
    <h3>The Challenge</h3>
      <h4>Step 1: Assessment</h4>
      <h4>Step 2: Response</h4>
  <h2>Day 2: The Gathering</h2>
    <h3>Introduction</h3>
 
<!-- ❌ Never skip heading levels -->
<h1>Title</h1>
<h3>Subtitle</h3> <!-- Skipped h2! -->

ARIA Landmarks (WCAG 1.3.1)

<body>
  <!-- Banner landmark -->
  <header role="banner">
    <nav role="navigation" aria-label="Main">
      <!-- Navigation links -->
    </nav>
  </header>
 
  <!-- Main landmark - one per page -->
  <main role="main">
    <!-- Primary content -->
  </main>
 
  <!-- Complementary landmark -->
  <aside role="complementary" aria-label="Progress">
    <!-- Progress information -->
  </aside>
 
  <!-- Contentinfo landmark -->
  <footer role="contentinfo">
    <!-- Footer content -->
  </footer>
 
  <!-- Search landmark (if applicable) -->
  <search role="search">
    <form>...</form>
  </search>
</body>

ARIA Labels and Descriptions

<!-- aria-label for elements without visible text -->
<button aria-label="Close experience">
  <svg aria-hidden="true">...</svg>
</button>
 
<!-- aria-labelledby for complex labeling -->
<div role="group" aria-labelledby="group-title group-desc">
  <h3 id="group-title">Threat Level Selection</h3>
  <p id="group-desc">Choose how challenging this experience will be</p>
  <!-- Radio buttons -->
</div>
 
<!-- aria-describedby for additional context -->
<input type="text" 
       id="username"
       aria-describedby="username-hint username-error"
       aria-required="true" />
<p id="username-hint">Enter your codename for this experience</p>
<p id="username-error" role="alert" aria-live="polite"></p>

Live Regions for Dynamic Content (WCAG 4.1.3)

<!-- Polite live region - announces when user is idle -->
<div aria-live="polite" aria-atomic="true" id="status-updates">
  <!-- Dynamic status messages -->
</div>
 
<!-- Assertive live region - interrupts immediately -->
<div aria-live="assertive" aria-atomic="true" id="critical-alerts">
  <!-- Critical notifications only -->
</div>
 
<!-- Log region for chat/history -->
<div role="log" aria-live="polite" aria-relevant="additions">
  <!-- Experience event log -->
</div>
 
<!-- Status region for progress updates -->
<div role="status" aria-live="polite" aria-atomic="true">
  <p>Experience saved successfully</p>
</div>

Progress Announcements

// Announce progress changes to screen readers
function announceProgress(current, total) {
  const announcement = document.createElement('div');
  announcement.setAttribute('role', 'status');
  announcement.setAttribute('aria-live', 'polite');
  announcement.className = 'visually-hidden';
  announcement.textContent = `Step ${current} of ${total} complete`;
  
  document.body.appendChild(announcement);
  
  // Remove after announcement
  setTimeout(() => announcement.remove(), 1000);
}
<!-- βœ… Descriptive button labels -->
<button>Start Day 1 Experience</button>
<button aria-describedby="save-desc">Save Progress</button>
<span id="save-desc" class="visually-hidden">Saves your current position in this experience</span>
 
<!-- βœ… Descriptive link text -->
<a href="/day-2">Continue to Day 2: The Gathering</a>
 
<!-- ❌ Avoid vague labels -->
<button>Click here</button> <!-- ❌ -->
<a href="/next">Read more</a> <!-- ❌ -->

Icon-Only Button Requirements

<!-- Icon-only buttons MUST have accessible names -->
<button aria-label="Play experience">
  <svg aria-hidden="true" focusable="false">
    <use href="#icon-play" />
  </svg>
</button>
 
<!-- Or use visually hidden text -->
<button>
  <svg aria-hidden="true" focusable="false">...</svg>
  <span class="visually-hidden">Pause experience</span>
</button>
 
<!-- Toggle buttons need aria-pressed -->
<button aria-pressed="false" aria-label="Mute audio">
  <svg aria-hidden="true">...</svg>
</button>

Form Labeling

<!-- βœ… Explicit label association -->
<label for="threat-name">Threat Name</label>
<input type="text" id="threat-name" name="threat-name" />
 
<!-- βœ… Implicit label (input inside label) -->
<label>
  Response Strategy
  <textarea name="strategy"></textarea>
</label>
 
<!-- βœ… aria-label when visible label isn't possible -->
<input type="search" aria-label="Search experiences" />
 
<!-- βœ… aria-labelledby for multiple labels -->
<span id="exp-label">Experience Duration</span>
<input type="number" aria-labelledby="exp-label exp-unit" />
<span id="exp-unit">minutes</span>
 
<!-- ❌ Never leave inputs unlabeled -->
<input type="text" placeholder="Enter name" /> <!-- ❌ -->

5. Motion & Animation

Respect prefers-reduced-motion (WCAG 2.3.3)

/* Default animations */
.bioluminescent-pulse {
  animation: pulse 2s ease-in-out infinite;
}
 
/* Respect user preference */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  
  .bioluminescent-pulse {
    animation: none;
    opacity: 1; /* Static state */
  }
}

Essential vs Non-Essential Animations

Animation TypeEssential?Reduced Motion Behavior
Progress indicatorsYesSimplified, no pulsing
Loading spinnersYesStatic icon or text
Success checkmarksNoInstant display
Background particlesNoRemove entirely
Hover transitionsNoInstant state change
Page transitionsNoFade only or instant
Threat pulse effectsContextualStatic glow instead

Animation Alternatives

// Check user preference before animating
const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;
 
function playThreatAnimation() {
  if (prefersReducedMotion) {
    // Show static representation
    showStaticThreatIndicator();
  } else {
    // Play full animation
    playFullAnimation();
  }
}

No Auto-Playing Content Without Pause (WCAG 1.4.2)

<!-- Auto-playing content must have pause control -->
<div class="ambient-audio">
  <audio autoplay loop>
    <source src="ambient.mp3" type="audio/mpeg" />
  </audio>
  <button aria-label="Pause ambient audio" aria-pressed="false">
    <svg aria-hidden="true">...</svg>
  </button>
</div>
 
<!-- Or provide pause before 3 seconds -->
<div class="auto-advance" data-auto-advance="3000">
  <button class="pause-advance" aria-label="Pause auto-advance">
    Pause
  </button>
  <!-- Content -->
</div>

6. Progress Indicators Accessibility

Progress Bar ARIA (WCAG 1.3.1, 4.1.2)

<!-- Linear progress bar -->
<div role="progressbar"
     aria-valuenow="35"
     aria-valuemin="0"
     aria-valuemax="100"
     aria-label="Experience progress"
     aria-describedby="progress-text">
  <div class="progress-track">
    <div class="progress-fill" style="width: 35%"></div>
  </div>
  <span id="progress-text" class="visually-hidden">
    35 percent complete
  </span>
</div>
 
<!-- Determinate progress (known duration) -->
<div role="progressbar"
     aria-valuenow="3"
     aria-valuemin="1"
     aria-valuemax="10"
     aria-label="Day progress">
</div>
 
<!-- Indeterminate progress (unknown duration) -->
<div role="progressbar"
     aria-label="Loading experience"
     aria-busy="true"
     aria-valuetext="Loading...">
</div>

Step Indicator Announcements

<!-- Multi-step progress indicator -->
<nav aria-label="Experience steps">
  <ol class="step-indicator">
    <li class="step-complete">
      <span class="visually-hidden">Completed:</span>
      <a href="#step-1">Assessment</a>
    </li>
    <li class="step-current" aria-current="step">
      <span class="visually-hidden">Current:</span>
      <span>Analysis</span>
    </li>
    <li class="step-pending">
      <span class="visually-hidden">Pending:</span>
      <span>Response</span>
    </li>
  </ol>
</nav>

Loading State Announcements

// Accessible loading state management
class LoadingManager {
  constructor(element) {
    this.element = element;
    this.liveRegion = document.createElement('div');
    this.liveRegion.setAttribute('aria-live', 'polite');
    this.liveRegion.setAttribute('aria-atomic', 'true');
    this.liveRegion.className = 'visually-hidden';
    document.body.appendChild(this.liveRegion);
  }
  
  start(message = 'Loading') {
    this.element.setAttribute('aria-busy', 'true');
    this.liveRegion.textContent = message;
  }
  
  update(progress, total) {
    const percent = Math.round((progress / total) * 100);
    this.liveRegion.textContent = `${percent} percent loaded`;
  }
  
  complete(message = 'Loading complete') {
    this.element.setAttribute('aria-busy', 'false');
    this.liveRegion.textContent = message;
    setTimeout(() => this.liveRegion.remove(), 1000);
  }
}

7. Modal/Dialog Accessibility

Focus Trap Implementation

// Focus trap for modals
class FocusTrap {
  constructor(element) {
    this.element = element;
    this.focusableSelectors = [
      'button:not([disabled])',
      'a[href]',
      'input:not([disabled])',
      'select:not([disabled])',
      'textarea:not([disabled])',
      '[tabindex]:not([tabindex="-1"])'
    ].join(', ');
  }
  
  get focusableElements() {
    return Array.from(
      this.element.querySelectorAll(this.focusableSelectors)
    );
  }
  
  trap(event) {
    const focusable = this.focusableElements;
    const first = focusable[0];
    const last = focusable[focusable.length - 1];
    
    if (event.key === 'Tab') {
      if (event.shiftKey && document.activeElement === first) {
        event.preventDefault();
        last.focus();
      } else if (!event.shiftKey && document.activeElement === last) {
        event.preventDefault();
        first.focus();
      }
    }
  }
}

Complete Modal Implementation

<dialog id="experience-modal"
        aria-labelledby="modal-title"
        aria-describedby="modal-desc"
        aria-modal="true"
        open>
  <header>
    <h2 id="modal-title">Experience Complete</h2>
    <button aria-label="Close modal" 
            onclick="closeModal()"
            class="close-button">
      <svg aria-hidden="true">...</svg>
    </button>
  </header>
  
  <div id="modal-desc">
    <p>You have successfully completed Day 1.</p>
    <p>Your choices have been recorded.</p>
  </div>
  
  <footer>
    <button onclick="replayExperience()">Replay</button>
    <button onclick="nextExperience()">Continue to Day 2</button>
  </footer>
</dialog>
// Modal controller
class AccessibleModal {
  constructor(dialog) {
    this.dialog = dialog;
    this.previousFocus = null;
    this.focusTrap = new FocusTrap(dialog);
  }
  
  open() {
    this.previousFocus = document.activeElement;
    this.dialog.showModal();
    
    // Set initial focus
    const title = this.dialog.querySelector('h2, h3');
    const firstButton = this.dialog.querySelector('button');
    (title || firstButton).focus();
    
    // Add event listeners
    this.dialog.addEventListener('keydown', (e) => {
      this.focusTrap.trap(e);
      if (e.key === 'Escape') {
        this.close();
      }
    });
  }
  
  close() {
    this.dialog.close();
    // Return focus to trigger
    if (this.previousFocus) {
      this.previousFocus.focus();
    }
  }
}
β–‘ aria-modal="true" on dialog element
β–‘ aria-labelledby pointing to title
β–‘ aria-describedby for content summary (optional)
β–‘ Focus trap implemented
β–‘ Initial focus on title or first interactive element
β–‘ Escape key closes modal
β–‘ Focus returns to trigger element on close
β–‘ Click outside closes (with option to disable)
β–‘ Background content is inert

8. Form Accessibility

Label Associations (WCAG 1.3.1, 3.3.2)

<!-- Explicit for/id association -->
<label for="email">Email Address</label>
<input type="email" id="email" name="email" required />
 
<!-- Grouped fields with fieldset/legend -->
<fieldset>
  <legend>Threat Response Priority</legend>
  <label>
    <input type="radio" name="priority" value="low" />
    Low Priority
  </label>
  <label>
    <input type="radio" name="priority" value="high" />
    High Priority
  </label>
</fieldset>

Error Message Linking (WCAG 3.3.1, 3.3.3)

<!-- Error messages linked with aria-describedby -->
<div class="form-field">
  <label for="username">Username</label>
  <input type="text" 
         id="username"
         name="username"
         required
         aria-required="true"
         aria-invalid="false"
         aria-describedby="username-error username-hint" />
  <span id="username-hint" class="hint">
    Choose a unique codename
  </span>
  <span id="username-error" class="error" role="alert" aria-live="assertive">
    <!-- Populated when error occurs -->
  </span>
</div>
// Error announcement function
function showError(input, message) {
  input.setAttribute('aria-invalid', 'true');
  const errorId = input.getAttribute('aria-describedby').split(' ')[0];
  const errorElement = document.getElementById(errorId);
  errorElement.textContent = message;
  
  // Announce to screen readers
  errorElement.setAttribute('role', 'alert');
}
 
function clearError(input) {
  input.setAttribute('aria-invalid', 'false');
  const errorId = input.getAttribute('aria-describedby').split(' ')[0];
  document.getElementById(errorId).textContent = '';
}

Required Field Indicators

<!-- Visible and programmatic required indicators -->
<label for="email">
  Email Address
  <span aria-label="required" class="required-indicator">*</span>
</label>
<input type="email" 
       id="email" 
       required 
       aria-required="true" />
 
<!-- Or use aria-label -->
<label for="password">
  Password
  <span class="required">(required)</span>
</label>
<input type="password" 
       id="password"
       required
       aria-required="true" />

Input Purpose (WCAG 1.3.5)

<!-- Autocomplete attributes for user data -->
<input type="text" 
       autocomplete="name" 
       aria-label="Full name" />
 
<input type="email" 
       autocomplete="email" 
       aria-label="Email address" />
 
<input type="tel" 
       autocomplete="tel" 
       aria-label="Phone number" />
 
<!-- Common autocomplete values -->
<!-- name, given-name, family-name, email, tel, 
     street-address, postal-code, country-name,
     username, current-password, new-password -->

Validation Announcements

// Form validation with screen reader announcements
class AccessibleForm {
  constructor(form) {
    this.form = form;
    this.errorSummary = document.createElement('div');
    this.errorSummary.setAttribute('role', 'alert');
    this.errorSummary.setAttribute('aria-live', 'assertive');
    this.errorSummary.className = 'error-summary visually-hidden';
    form.prepend(this.errorSummary);
  }
  
  validate() {
    const errors = [];
    let firstInvalid = null;
    
    // Validate each field
    this.form.querySelectorAll('[required]').forEach(field => {
      if (!field.value.trim()) {
        const label = document.querySelector(`label[for="${field.id}"]`);
        const fieldName = label ? label.textContent : field.name;
        errors.push(`${fieldName} is required`);
        
        if (!firstInvalid) firstInvalid = field;
        this.showFieldError(field, 'This field is required');
      }
    });
    
    if (errors.length > 0) {
      // Announce errors
      this.errorSummary.textContent = 
        `Form has ${errors.length} errors: ${errors.join(', ')}`;
      
      // Focus first invalid field
      firstInvalid.focus();
      return false;
    }
    
    return true;
  }
}

9. Touch & Mobile Accessibility

Touch Target Sizes (WCAG 2.5.5)

Minimum 44x44 CSS pixels for all interactive elements:

/* Minimum touch target size */
button,
a,
input,
select,
textarea,
[role="button"] {
  min-width: 44px;
  min-height: 44px;
}
 
/* Compact buttons with expanded touch area */
.compact-button {
  position: relative;
  width: 32px;
  height: 32px;
}
 
.compact-button::after {
  content: '';
  position: absolute;
  inset: -6px; /* Expands touch area to 44px */
}
 
/* Spacing between touch targets */
.touch-target-group {
  display: flex;
  gap: 8px; /* Minimum spacing */
}

Gesture Alternatives (WCAG 2.5.1)

// Provide alternatives to complex gestures
class GestureManager {
  constructor(element) {
    this.element = element;
  }
  
  // Swipe gesture with button alternative
  setupSwipe() {
    // Swipe detection
    let startX = 0;
    this.element.addEventListener('touchstart', (e) => {
      startX = e.touches[0].clientX;
    });
    
    this.element.addEventListener('touchend', (e) => {
      const endX = e.changedTouches[0].clientX;
      const diff = startX - endX;
      
      if (Math.abs(diff) > 50) {
        if (diff > 0) {
          this.next(); // Swipe left
        } else {
          this.previous(); // Swipe right
        }
      }
    });
  }
  
  // Button alternatives
  addButtonAlternatives() {
    const prevBtn = document.createElement('button');
    prevBtn.innerHTML = '&larr; Previous';
    prevBtn.onclick = () => this.previous();
    
    const nextBtn = document.createElement('button');
    nextBtn.innerHTML = 'Next &rarr;';
    nextBtn.onclick = () => this.next();
    
    this.element.append(prevBtn, nextBtn);
  }
}

Orientation Support (WCAG 1.3.4)

/* Support both orientations */
@supports not (orientation: landscape) {
  /* Fallback for devices that lock orientation */
}
 
/* Ensure content works in both orientations */
.experience-container {
  /* Mobile portrait */
  @media (orientation: portrait) {
    flex-direction: column;
  }
  
  /* Mobile landscape / desktop */
  @media (orientation: landscape) {
    flex-direction: row;
  }
}
 
/* Never lock orientation via CSS or JS */
/* ❌ screen.orientation.lock('portrait') */

Zoom Without Loss of Functionality (WCAG 1.4.4, 1.4.10)

/* Allow zoom up to 400% */
html {
  /* Never disable zoom */
  /* ❌ user-scalable=no */
}
 
/* Responsive at 320px equivalent (400% of 1280px) */
@media (max-width: 320px) {
  .experience-content {
    /* Reflow content vertically */
    display: flex;
    flex-direction: column;
  }
  
  /* Ensure no horizontal scroll */
  body {
    overflow-x: hidden;
  }
}
 
/* Reflow requirements */
.experience-container {
  /* Content must reflow without horizontal scroll */
  max-width: 100%;
  
  /* No content loss at 400% zoom */
  min-height: auto;
  
  /* Vertical scrolling only */
  overflow-y: auto;
  overflow-x: hidden;
}

10. Testing Checklist

Automated Testing Tools

ToolTypeCoverage
axe DevToolsBrowser extensionWCAG 2.1 AA
LighthouseBuilt-in ChromeBasic a11y
WAVEBrowser extensionVisual feedback
Pa11yCLI/CIAutomated scanning
eslint-plugin-jsx-a11yLintingReact a11y

Manual Keyboard Testing

β–‘ Tab through entire page - is order logical?
β–‘ Shift+Tab moves backwards correctly
β–‘ All interactive elements reachable via Tab
β–‘ No keyboard traps
β–‘ Focus visible on all elements
β–‘ Enter activates buttons/links
β–‘ Space activates buttons
β–‘ Arrow keys work in widgets
β–‘ Escape closes modals/menus
β–‘ Home/End work in lists
β–‘ Page Up/Down scroll content

Screen Reader Testing

Screen ReaderBrowserPlatform
NVDAFirefox, ChromeWindows
JAWSChrome, EdgeWindows
VoiceOverSafarimacOS, iOS
TalkBackChromeAndroid
NarratorEdgeWindows

Testing Commands:

NVDA:
- Insert + F7: Elements list
- Insert + Space: Focus/browse mode
- H: Next heading
- D: Next landmark
- T: Next table
- F: Next form field

VoiceOver:
- Cmd + F5: Toggle VoiceOver
- VO + U: Rotor
- VO + Cmd + H: Next heading
- VO + Cmd + L: Next link

Screen Reader Testing Checklist

β–‘ Page title announced on load
β–‘ Heading hierarchy makes sense
β–‘ Landmarks navigable
β–‘ All images have alt text
β–‘ Form labels announced correctly
β–‘ Error messages announced
β–‘ Buttons have accessible names
β–‘ Links are descriptive
β–‘ Live regions announce updates
β–‘ Progress changes announced
β–‘ Modal content accessible
β–‘ Tables have headers
β–‘ Lists properly structured

Color Contrast Verification

β–‘ Normal text (4.5:1 minimum)
β–‘ Large text (3:1 minimum)
β–‘ UI components (3:1 minimum)
β–‘ Focus indicators (3:1 minimum)
β–‘ Graphs/charts don't rely on color alone
β–‘ Error states have more than color
β–‘ Success states have more than color

Cognitive Accessibility Considerations

β–‘ Clear, simple language
β–‘ Consistent navigation
β–‘ Predictable behavior
β–‘ Error prevention
β–‘ Clear error messages
β–‘ No time limits (or adjustable)
β–‘ No flashing content (>3Hz)
β–‘ Clear focus indicators
β–‘ Help text available
β–‘ Consistent icon usage

11. Accessibility Statement Template

# Accessibility Statement
 
## 40 Days of Existential Threats
 
**Last Updated:** [Date]
 
### Our Commitment
 
40 Days of Existential Threats is committed to ensuring digital accessibility 
for people with disabilities. We are continually improving the user experience 
for everyone and applying the relevant accessibility standards.
 
### Conformance Status
 
This website is partially conformant with WCAG 2.1 Level AA. Partially 
conformant means that some parts of the content do not fully conform to 
the accessibility standard.
 
### Accessibility Features
 
- Keyboard navigation support for all interactive elements
- Screen reader compatibility with ARIA landmarks and labels
- Color contrast ratios meeting WCAG 2.1 AA standards
- Reduced motion support for animations
- Resizable text up to 400% without loss of functionality
- Alternative text for all images
- Descriptive link text and button labels
 
### Known Limitations
 
- [List any known accessibility issues]
- [Describe workarounds if available]
- [Timeline for resolution]
 
### Feedback
 
We welcome your feedback on the accessibility of 40 Days of Existential Threats.
 
- Email: [[email protected]]
- Phone: [Phone number with accessibility support]
- Address: [Physical address]
 
We aim to respond to accessibility feedback within [timeframe] business days.
 
### Compatibility
 
40 Days of Existential Threats is designed to be compatible with:
 
- Current versions of Chrome, Firefox, Safari, and Edge
- Screen readers including NVDA, JAWS, and VoiceOver
- Operating system accessibility features
 
### Assessment
 
Accessibility of 40 Days of Existential Threats is assessed by:
 
- Self-evaluation using automated testing tools
- Manual testing with assistive technologies
- [Third-party assessment if applicable]
 
### Formal Complaints
 
If you are not satisfied with our response to your accessibility concerns, 
you may contact [relevant authority or ombudsman].
 
### Technical Specifications
 
Accessibility of 40 Days of Existential Threats relies on the following 
technologies:
 
- HTML5
- WAI-ARIA
- CSS
- JavaScript
 
These technologies are relied upon for conformance with the accessibility 
standards used.
 
---
 
This statement was created using the W3C Accessibility Statement Generator Tool.

Quick Reference: ARIA Attributes by Use Case

Use CaseARIA Attributes
Buttonsaria-pressed, aria-expanded, aria-haspopup
Linksaria-current, aria-label
Formsaria-required, aria-invalid, aria-describedby
Progressaria-valuenow, aria-valuemin, aria-valuemax, aria-valuetext
Live Contentaria-live, aria-atomic, aria-relevant
Navigationaria-label, aria-current, aria-expanded
Tabsaria-selected, aria-controls, aria-labelledby
Modalsaria-modal, aria-labelledby, aria-describedby
Alertsrole="alert", role="status", aria-live

WCAG 2.1 AA Criteria Quick Reference

CriterionLevelDescription
1.1.1ANon-text Content
1.2.1-1.2.5A/AATime-based Media
1.3.1-1.3.5A/AAAdaptable
1.4.1-1.4.13A/AADistinguishable
2.1.1-2.1.4AKeyboard Accessible
2.2.1-2.2.2AEnough Time
2.3.1-2.3.3A/AAASeizures/Physical Reactions
2.4.1-2.4.11A/AANavigable
2.5.1-2.5.6A/AAInput Modalities
3.1.1-3.1.2A/AAReadable
3.2.1-3.2.6A/AAPredictable
3.3.1-3.3.6A/AAInput Assistance
4.1.1-4.1.3A/AACompatible

Document Version: 1.0 Created for: 40 Days of Existential Threats Compliance Target: WCAG 2.1 Level AA