- Add aria-labels to menu action buttons (PDF, Print, Contact) - Add aria-labelledby to toggle checkboxes (desktop + mobile) - Add -webkit-user-select prefix for Safari compatibility - Add DynamicCacheControl middleware for HTML pages - Add accessibility test suite (60-accessibility.test.mjs) - Add comprehensive accessibility documentation (21-ACCESSIBILITY.md) - Update Modern Web Techniques doc to mark audit complete
12 KiB
Accessibility Guide
WCAG 2.1 AA Compliance Documentation Last Updated: December 2025
Overview
This document describes the accessibility features implemented in the CV website to ensure WCAG 2.1 AA compliance and provide an inclusive user experience.
Table of Contents
- Implemented Features
- Button Accessibility
- Form Elements
- Keyboard Navigation
- Screen Reader Support
- CSS Compatibility
- HTTP Headers
- Testing
- Checklist
Implemented Features
Quick Summary
| Feature | Status | Notes |
|---|---|---|
| Button aria-labels | ✅ Complete | All buttons have discernible text |
| Form labels | ✅ Complete | All inputs have aria-labelledby |
| Keyboard navigation | ✅ Complete | Tab, Enter, Escape support |
| Modal accessibility | ✅ Complete | Native <dialog> with close buttons |
| Color themes | ✅ Complete | Light/Dark/Auto modes |
| Screen reader | ✅ Complete | Live regions for announcements |
| CSS prefixes | ✅ Complete | Safari/WebKit compatibility |
| Security headers | ✅ Complete | X-Content-Type-Options, CSP |
| Cache headers | ✅ Complete | Static and dynamic routes |
Button Accessibility
All interactive buttons include proper accessibility attributes:
Fixed Action Buttons
Located in templates/partials/widgets/:
<!-- Download PDF Button -->
<button id="download-button"
aria-label="{{.UI.Widgets.Download.AriaLabel}}"
data-tooltip="{{.UI.Widgets.Download.Tooltip}}">
<iconify-icon icon="catppuccin:pdf"></iconify-icon>
</button>
<!-- Print-Friendly Button -->
<button id="print-friendly-button"
aria-label="{{.UI.Widgets.Print.AriaLabel}}"
data-tooltip="{{.UI.Widgets.Print.Tooltip}}">
<iconify-icon icon="mdi:leaf"></iconify-icon>
</button>
<!-- Shortcuts Button -->
<button id="shortcuts-button"
aria-label="{{.UI.Widgets.Shortcuts.AriaLabel}}"
data-tooltip="{{.UI.Widgets.Shortcuts.Tooltip}}">
<iconify-icon icon="mdi:keyboard-outline"></iconify-icon>
</button>
Mobile Menu Buttons
Located in templates/partials/navigation/hamburger-menu.html:
<!-- All menu action buttons have aria-labels -->
<button class="menu-action-btn menu-pdf-btn"
aria-label="{{.UI.Widgets.ActionButtons.DownloadPdf}}">
<iconify-icon icon="catppuccin:pdf"></iconify-icon>
<span>{{.UI.Widgets.ActionButtons.DownloadPdf}}</span>
</button>
Best Practices
- Icon-only buttons: Always include
aria-label - Buttons with text: Visible text serves as the accessible name
- Tooltips: Use
data-tooltipfor visual hint,aria-labelfor screen readers
Form Elements
All form inputs have proper label associations:
Toggle Checkboxes
Desktop toggles in templates/partials/navigation/view-controls.html:
<div class="selector-group" id="desktop-length-toggle">
<label class="selector-label" id="length-toggle-label">
{{.UI.ViewControls.Length}}:
</label>
<label class="icon-toggle">
<input type="checkbox"
id="lengthToggle"
aria-labelledby="length-toggle-label"
aria-describedby="length-toggle-desc">
<span class="icon-toggle-slider">...</span>
<span id="length-toggle-desc" class="sr-only">
{{.UI.ViewControls.LengthDescription}}
</span>
</label>
</div>
Mobile toggles in templates/partials/navigation/hamburger-menu.html:
<div class="menu-control-item" id="mobile-length-toggle">
<label class="menu-control-label" id="menu-length-toggle-label">
<iconify-icon icon="mdi:file-document-outline"></iconify-icon>
<span>{{.UI.ViewControls.Length}}</span>
</label>
<label class="icon-toggle">
<input type="checkbox"
id="lengthToggleMenu"
aria-labelledby="menu-length-toggle-label">
<span class="icon-toggle-slider">...</span>
</label>
</div>
Contact Form
Located in templates/partials/modals/contact-modal.html:
<div class="form-group">
<label for="contact-email" class="form-label">
{{.UI.ContactModal.Form.Email}}
<span class="required-indicator">*</span>
</label>
<input type="email"
id="contact-email"
name="email"
required
aria-required="true"
placeholder="{{.UI.ContactModal.Form.EmailPlaceholder}}">
</div>
Labeling Strategies
| Strategy | When to Use |
|---|---|
<label for="id"> |
Standard form inputs |
aria-labelledby |
Complex widgets, toggles |
aria-describedby |
Additional context/descriptions |
aria-label |
When no visible label exists |
Keyboard Navigation
Supported Shortcuts
| Key | Action |
|---|---|
Tab |
Move focus to next element |
Shift+Tab |
Move focus to previous element |
Enter / Space |
Activate focused button/link |
Escape |
Close modals |
? |
Open shortcuts modal |
Ctrl/Cmd + K |
Open command palette |
Ctrl/Cmd + P |
Print friendly version |
Ctrl/Cmd + +/-/0 |
Zoom controls |
Focus Management
- All interactive elements are focusable
- Focus is trapped inside open modals
- Focus returns to trigger element when modal closes
- Skip links available for screen reader users
Implementation
<!-- Modal with keyboard support -->
<dialog id="shortcuts-modal" class="info-modal"
_="on click call closeOnBackdrop(me, event)">
<!-- Press Escape to close (native dialog behavior) -->
<button class="info-modal-close"
commandfor="shortcuts-modal"
command="close"
aria-label="{{.UI.ShortcutsModal.Close}}">
<iconify-icon icon="mdi:close"></iconify-icon>
</button>
</dialog>
Screen Reader Support
Live Regions
Announcements for dynamic content changes:
<!-- Loading indicator -->
<span id="loading"
role="status"
aria-live="polite"
aria-label="Loading">
<span class="loader"></span>
</span>
<!-- PDF selection announcement -->
<div id="pdf-selection-announcement"
class="sr-only"
aria-live="polite"
aria-atomic="true"></div>
<!-- Contact form response -->
<div id="contact-response"
class="contact-response"
role="status"
aria-live="polite"></div>
Screen Reader Only Text
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
ARIA Landmarks
<!-- Navigation -->
<nav role="navigation" aria-label="CV sections">...</nav>
<div class="action-bar" role="navigation" aria-label="Language and export controls">...</div>
<!-- Zoom control -->
<div id="zoom-control" role="group" aria-label="{{.UI.Widgets.ZoomControl.GroupLabel}}">
<input type="range"
aria-label="{{.UI.Widgets.ZoomControl.SliderLabel}}"
aria-valuemin="25"
aria-valuemax="300"
aria-valuenow="100"
aria-valuetext="100%">
</div>
CSS Compatibility
Browser Prefixes
All CSS properties with limited browser support include vendor prefixes:
/* User selection prevention */
.toggle-switch {
-webkit-user-select: none; /* Safari */
user-select: none;
}
/* Backdrop blur effect */
.zoom-control {
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); /* Safari */
}
Files Updated
| File | Property Fixed |
|---|---|
_toggles.css |
-webkit-user-select |
_zoom-control.css |
-webkit-user-select |
_sidebar.css |
-webkit-user-select |
_cv-section.css |
-webkit-user-select |
_breakpoints.css |
-webkit-user-select |
_toasts.css |
-webkit-backdrop-filter (already present) |
_modals.css |
-webkit-backdrop-filter (already present) |
Feature Detection
For backdrop-filter, use @supports:
@supports (backdrop-filter: blur(20px)) or (-webkit-backdrop-filter: blur(20px)) {
.blur-bar {
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
}
}
HTTP Headers
Security Headers
Implemented in internal/middleware/security.go:
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Prevent MIME type sniffing
w.Header().Set("X-Content-Type-Options", "nosniff")
// Prevent clickjacking
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
// XSS Protection
w.Header().Set("X-XSS-Protection", "1; mode=block")
// Referrer policy
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Content Security Policy
w.Header().Set("Content-Security-Policy", "...")
// HSTS (production only)
if os.Getenv("GO_ENV") == "production" {
w.Header().Set("Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload")
}
next.ServeHTTP(w, r)
})
}
Cache Control
Static Files (CSS, JS, images):
// 1 hour dev, 1 day production
w.Header().Set("Cache-Control", "public, max-age=86400")
Dynamic Routes (HTML pages):
// Production: 5 minutes with must-revalidate
w.Header().Set("Cache-Control", "public, max-age=300, must-revalidate")
// Development: no cache
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
Testing
Running Accessibility Tests
# Run accessibility test suite
bun run tests/mjs/60-accessibility.test.mjs
# Or with the test runner
cd tests && bun run run-all.mjs
Test Coverage
The 60-accessibility.test.mjs file tests:
- HTTP Security Headers - X-Content-Type-Options, X-Frame-Options, CSP
- Cache-Control Headers - Presence and correct values
- Buttons with Discernible Text - All buttons have aria-label or visible text
- Form Elements with Labels - All inputs have associated labels
- Toggle Checkboxes - aria-labelledby with valid linked elements
- ARIA Landmarks - Navigation, main, dialog elements
- Keyboard Navigation - Focusable interactive elements
- Modal Accessibility - Close buttons, aria attributes
- Color Theme Support - Theme switcher availability
- Screen Reader Announcements - Live regions for dynamic content
Manual Testing
- Keyboard-only navigation: Tab through all interactive elements
- Screen reader testing: Use VoiceOver (macOS) or NVDA (Windows)
- High contrast mode: Test visibility in Windows High Contrast
- Zoom testing: Test at 200% browser zoom
Accessibility Checklist
Before Each Release
- Run
60-accessibility.test.mjs- all tests pass - Test keyboard navigation (Tab, Enter, Escape)
- Verify all buttons have aria-labels
- Check form inputs have labels
- Test with screen reader
- Verify modals trap focus
- Check color contrast ratios
- Test at 200% zoom
WCAG 2.1 AA Requirements
| Criterion | Status | Implementation |
|---|---|---|
| 1.1.1 Non-text Content | ✅ | Alt text, aria-labels |
| 1.3.1 Info and Relationships | ✅ | Semantic HTML, ARIA |
| 1.4.3 Contrast (Minimum) | ✅ | Theme system |
| 2.1.1 Keyboard | ✅ | Full keyboard support |
| 2.1.2 No Keyboard Trap | ✅ | Modal focus management |
| 2.4.1 Bypass Blocks | ✅ | Skip links, landmarks |
| 2.4.4 Link Purpose | ✅ | Descriptive link text |
| 2.4.6 Headings and Labels | ✅ | Semantic structure |
| 3.2.1 On Focus | ✅ | No unexpected changes |
| 4.1.2 Name, Role, Value | ✅ | ARIA attributes |
Resources
- WCAG 2.1 Guidelines
- ARIA Authoring Practices
- MDN Accessibility Guide
- Can I Use - CSS Browser Support
Changelog
December 2025
- Added aria-labels to menu action buttons (PDF, Print, Contact)
- Added aria-labelledby to all toggle checkboxes (desktop and mobile)
- Added -webkit-user-select prefix for Safari compatibility
- Added DynamicCacheControl middleware for HTML pages
- Created comprehensive accessibility test suite
- Created this documentation