Files

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" and aria-live="polite"
  • Tables — every <th> must have scope="col" (or scope="row")
  • Buttons<button> elements need accessible text (icon-only buttons need aria-label)
  • Navigation toggle — hamburger button needs aria-label="Toggle navigation" and aria-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:hover has border-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: pointer and hover highlight — no :focus outline removed

Landmarks

  • <div id="nav"> → nav injected via fetch('/nav.html'), already has role="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:

  1. Place nav JS in /js/utils.js as window.initNav()
  2. 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-path attribute)
  • Hamburger toggle (aria-expanded management)
  • 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