Files
sortof/docs/a11y-audit-2026-05-01.md

9.4 KiB
Raw Blame History

Frontend a11y audit — 2026-05-01

Surfaces audited: index.html (CSS theme + components), sortof-app.jsx (~1467 lines), tweaks-panel.jsx (dev-only, gated behind ?tweaks=1).

Scope: every UI surface where state, severity, or category is conveyed to the user. Findings ranked by user impact.

Palette decision

Okabe-Ito (CVD-safe) lifted to dark-mode lightness so each accent color clears ≥4.5:1 against --bg-1 (oklch 0.21). Hues kept at Okabe-Ito's chosen 30°-spaced anchors so the pairs (red/green especially) remain distinguishable under deuteranopia/protanopia/tritanopia.

--acc-success: oklch(0.78 0.13 165)   /* bluish-green, "Okabe-Ito green" lifted */
--acc-warn:    oklch(0.82 0.15 75)    /* orange-yellow, warning */
--acc-error:   oklch(0.70 0.18 35)    /* vermillion, error */
--acc-info:    oklch(0.78 0.13 230)   /* sky blue, info/links/buttons */

Old --acc-green (yellow-green, hue 155) rolls over to --acc-success. Old --acc-red (hue 25) → --acc-error (hue 35, more chroma). Old --acc-amber and --acc-blue keep their hues but bump chroma. Names changed from green/amber/red/blue (hue-based) to success/warn/error/info (semantic) so accidental "green = good" coupling breaks; backwards-compat aliases keep existing references working until next sweep.

Foreground ramp lift

Current --fg-3 (oklch 0.45) on --bg-1 (oklch 0.21) is ~2.7:1 — WCAG AA fail for body text. Used for tagline, label-meta, branch-name, code-block muted text, footer, etc. — that's the "gray on gray" the user reported.

Token Before After Used for
--fg 0.95 0.95 (unchanged) Primary text
--fg-1 0.78 0.82 Secondary text (panel headings)
--fg-2 0.60 0.72 Tertiary text (labels, tags). Now ~5.5:1 — passes AA.
--fg-3 0.45 0.60 Decorative only — chevrons, separators, dot-leds. Now ~4.4:1 — passes AA for non-text usage.

Rule: any element with readable text content (>1 word) uses --fg-2 minimum. --fg-3 is reserved for non-text decoration (chevrons, divider dots, separators). Audited each prior --fg-3 usage and reclassified accordingly (see fix list below).

Findings

Critical (color-only signals, fail per AudioEye rule "never rely on color alone")

# Where Current Fix
C1 .status-pill.cached/queued/parse/expanding/unknown/nonmod (index.html:302-314) Color-coded label + tiny dot. The label text says "12 cached" / "5 queued" — that's a count + role label, but the role itself is text. Acceptable per AudioEye rule (count ≠ encoding). However the dot-led is purely color. Add a per-state glyph prefix to the dot-led: (cached), (queued, blink), (draining), (expanding), ? (unknown), (nonmod). Glyph is the primary signal; color reinforces.
C2 StatusStrip terminal pill text — state === 'failed' shows "job failed" with .idle class (index.html:308 + sortof-app.jsx:231) Plain text, no icon. Indistinguishable from idle ("ready when you are") at a glance. Prefix for failed, for done/success, for cold, for idle.
C3 .warn-section .badge red vs amber (index.html:578-592) Red and amber rounded badges visually differ only by hue. Palette change helps but adds no glyph. Prefix the count with ! (red/error) or (amber/warn). Both badges become ! 3 or ⚠ 2 — count is still the load-bearing info, glyph disambiguates severity.
C4 .warn-list .w-tag red vs amber (index.html:621-622) Tag text is colored ("MISSING" red, "CYCLE" amber). Tag itself is the label; color is reinforcement. Acceptable. But the tag color alone distinguishes severity within the list. Add a leading glyph: ! MISSING, ⚠ CYCLE, ⚠ CONFLICT.
C5 .copy-btn.copied (index.html:409) Pure color shift to green. The IconCheck glyph IS shown when copied (sortof-app.jsx:130), so glyph signal exists. Acceptable. Verify the IconCheck strokes pick up currentColor so palette change propagates.
C6 .warn-branch-btn.picked (index.html:695) Pure color shift to green + star prefix in JSX (sortof-app.jsx:411). ★ glyph already encodes "picked"; pass.
C7 .diff-stat.add/rm/mv (index.html:495-498) Color + the prefix glyphs +, , in JSX. Glyphs already encode meaning; color reinforces; pass.
C8 err-banner and cold-banner (sortof-app.jsx:680-688, 666-673) "err" / "cold" text tag + message. No icon. The amber/red border conveys severity. Add glyph to err-tag content, to cold-tag (or ).
C9 .sort-btn[disabled] (index.html:269-274) Opacity drop + line color. No iconic disabled signal. Acceptable (dimmed appearance plus the disabled cursor is the convention). Verify disabled attr is set, not just CSS.
C10 .cancel-btn hover state (index.html:327) Hover turns red. No glyph. Add prefix to button label: ✗ cancel. Already-red on hover then doesn't carry meaning alone.

Important (focus, hover, contrast)

# Where Current Fix
I1 No :focus-visible rules anywhere Browser-default focus ring on inputs only; buttons get nothing on keyboard nav. Add a global :focus-visible rule with 2px outline + 2px offset, color --acc-info, applied to all interactive elements (button, a, [role="button"], input, textarea, select, summary, [tabindex]).
I2 Hover-only color shifts on chrome elements (e.g. .icon-btn:hover) Color contrast pre-hover may pass; mouse-only convention. Pair hover with subtle background tint (already in some places), keep.
I3 .tagline color: var(--fg-3) (index.html:102) — gray on gray. Tagline is decorative; ratio fails AA but content is non-essential. Lift to --fg-2 per the ramp rule.
I4 .label-meta color: var(--fg-3) (index.html:190) — line meta info "12 lines" is informational text. Bump to --fg-2.
I5 .branch-name var(--fg-3) (index.html:448) — mod display name in picker Information-bearing text. Bump to --fg-2.
I6 .branch-deps/.branch-pos var(--fg-3) (index.html:449-450) Information-bearing. Bump to --fg-2.
I7 code-block .ink-sep/.ink-mut var(--fg-3) (index.html:389-390) Code separator/muted ink — decorative + structural. Lifted --fg-3 (0.60) is now AA-OK; keep.
I8 Footer: var(--fg-3) (index.html:140) — text "based on..." / "a thing by..." Information-bearing text. Bump to --fg-2.
I9 .cb-meta, .cb-key, table count, table .chev all --fg-3 Mixed. Counts and keys are information; chevrons are decoration. Bump information-bearing to --fg-2.
I10 .warn-list .w-tag (default, no level class): var(--fg-2) Already at fg-2; passes after the ramp lift.

Minor (cosmetic, nice-to-have)

# Where Current Fix
M1 Links: border-bottom: 1px dotted (index.html:65) Underline equivalent. OK. Change dotted → solid on hover for sharper feedback.
M2 cancel-btn no width-match for sort-btn neighbors Cosmetic. Skip.
M3 .cat.patch/.cat.map/.cat.lib pills Pill text is the label ("patch"/"map"/"lib"); color is reinforcement. Verify new palette mappings hold.

Out-of-scope / requires backend support

None. All findings are frontend-resolvable.

Per-component fix triples (post-fix state signals)

Each interactive component now emits at minimum (color, icon/glyph, text):

Component Color Glyph/Icon Text
Status pill (cached) success "12 cached"
Status pill (queued) warn (blinking) "5 queued"
Status pill (draining) info (blinking) "3 draining"
Status pill (expanding) info (blinking) "expanding collection…"
Status pill (unknown) error ? "1 unknown"
Status pill (nonmod) fg-2 "1 non-mod"
Status pill (idle) fg-2 "ready when you are"
Status pill (done) success "done. N mods, W warnings"
Status pill (failed) error "job failed"
Status pill (cold) warn "cache miss — be patient"
Status pill (error) error "something went sideways"
Warning badge (red) error ! "3"
Warning badge (amber) warn "2"
Warning row (missing) error ! "MISSING" + msg
Warning row (cycle/conflict) warn "CYCLE"/"CONFLICT" + msg
Err banner error "err" + msg + retry button
Cold banner warn "cold" + msg
Cancel button error (hover) "cancel"
Copy button (default) info IconCopy "copy"
Copy button (copied) success IconCheck "copied"
Branch row (picked) success (in label) mod_id text
Diff stat (add) success + count
Diff stat (rm) error count
Diff stat (mv) warn count

Acceptance check

  • Every state signal is now (color × glyph × text); never color alone.
  • All accent colors clear ≥4.5:1 against --bg-1.
  • All text-bearing fg tokens (--fg, --fg-1, --fg-2) clear ≥4.5:1.
  • --fg-3 reserved for non-text decoration, lifted to ~4.4:1 anyway as a hedge.
  • Focus rings present on every interactive element.
  • Links remain underlined.
  • Form-error pattern (red border + ⚠ + text) — N/A: no form validation surface in this app today; spec'd for future.

Implementation: see /opt/sortof/docs/a11y-changes-2026-05-01.md for the file-by-file diff summary.