4.4 KiB
4.4 KiB
Web Standards & Accessibility — Ops Portal Checklist
Applied during the July 2026 audit to all 10 ops portal HTML files. Covers HTML5 validity, WCAG 2.1 AA, and ARIA compliance.
Structural Requirements
Document-Level
<!DOCTYPE html>(first line)<html lang="en"><meta charset="UTF-8">(first element in<head>)<meta name="viewport" content="width=device-width, initial-scale=1.0">
Semantic Elements
| Element | When to Use |
|---|---|
<main id="main-content" role="main"> |
One per page, wraps primary content |
<header> |
Page header (h1 + actions) |
<footer> |
Page footer (last-updated timestamp) |
<nav> / <nav role="navigation" aria-label="Main navigation"> |
Navigation block |
<section class="card" aria-labelledby="sectionId"> |
Every card/section, paired with <h2 id="sectionId"> |
<h1> → <h2> → <h3> |
Strict hierarchy, no skipping |
Section Card Pattern (before → after)
Before (invalid):
<div class="card">
<div class="card-header">Section Title</div>
...
</div>
After (valid):
<section class="card" aria-labelledby="myHeading">
<h2 class="card-header" id="myHeading">Section Title</h2>
...
</section>
Accessibility Requirements
Interactive Elements
- SVG icons — all decorative/informational SVGs need:
role="img"aria-label="Refresh"(or equivalent descriptive text)
- Error banners — must have
role="alert"andaria-live="polite" - Tables — every
<th>must havescope="col"(orscope="row") - Buttons —
<button>elements need accessible text (icon-only buttons needaria-label) - Navigation toggle — hamburger button needs
aria-label="Toggle navigation"andaria-expanded
Color & Contrast
- The dark theme palette passes WCAG AA:
--bg-body: #0f172a/--text-primary: #f1f5f9→ ratio 15.1:1--accent: #f59e0b(amber) on--bg-card: #1e293b→ ratio 7.2:1- Status colors (green
#22c55e, red#ef4444) used only as supplements, not sole indicators - Status dots (
.status-dot) are always paired with text labels
Focus & Keyboard
.refresh-btn:hoverhasborder-color: var(--accent)— also applies:focus-visible(browser default).filter-btn.active— selection state is conveyed through text (class change + visible count)- Sortable table headers use
cursor: pointerand hover highlight — no:focusoutline removed
Landmarks
<div id="nav">→ nav injected viafetch('/nav.html'), already hasrole="navigation" aria-label="Main navigation"<main id="main-content" role="main">— primary content landmark<header>— implicit banner landmark<footer>— implicit contentinfo landmark
Nav Injection Pattern (Critical)
Inline <script> in nav.html does not execute when the file is loaded via innerHTML. Always:
- Place nav JS in
/js/utils.jsaswindow.initNav() - Call
initNav()after every nav injection:
fetch('/nav.html')
.then(r => r.text())
.then(html => {
document.getElementById('nav').innerHTML = html;
if (typeof window.initNav === 'function') window.initNav();
})
initNav() handles:
- Active link highlighting (via
data-pathattribute) - Hamburger toggle (
aria-expandedmanagement) - Click-outside-to-close
Verification Commands
# Structural balance check
for section balance:
grep -Pc '<section\b' file.html
grep -c '</section>' file.html
for main balance:
grep -c '<main' file.html
grep -c '</main>' file.html
for h2 balance:
grep -Pc '<h2\b' file.html
grep -c '</h2>' file.html
Common Fixes
| Issue | Fix |
|---|---|
<div class="card"> → <section> |
Change opening to <section class="card" aria-labelledby="id">, closing to </section> |
.card-header → <h2> |
Change <div class="card-header"> to <h2 class="card-header" id="id">, closing to </h2> |
</div> should be </section> |
Convert all section-closing </div> to </section> |
SVG missing role="img" |
Add role="img" and aria-label="..." |
| Error banner missing ARIA | Add role="alert" aria-live="polite" |
<th> missing scope |
Add scope="col" to every <th> |
No <main> wrapper |
Wrap primary content in <main id="main-content" role="main"> |
| Inline nav script | Move to /js/utils.js as window.initNav(), call after injection |