Skip to content

ui: a11y fixes from web-design-guidelines audit + transition: all sweep#72

Open
Kures wants to merge 2 commits into
nullclaw:mainfrom
Kures:feat/ui-a11y-fixes
Open

ui: a11y fixes from web-design-guidelines audit + transition: all sweep#72
Kures wants to merge 2 commits into
nullclaw:mainfrom
Kures:feat/ui-a11y-fixes

Conversation

@Kures
Copy link
Copy Markdown

@Kures Kures commented May 12, 2026

PR7 — ui: a11y fixes from web-design-guidelines audit + transition: all sweep

Repository: nullclaw/nullhub
Branch: feat/ui-a11y-fixes

Two commits, 29 files, no behaviour change. Pure accessibility,
keyboard navigation, and CSS-correctness polish — no business logic
touched, no redesign.

Why

Ran the Web Interface Guidelines audit (vercel-labs/web-interface-guidelines)
over ui/src/ and found ~48 issues. The top 5 affect real users:

  1. Keyboard users can't see focus. The global rule
    input, button, textarea { outline: none } is applied without any
    :focus-visible replacement, so Tab navigation moves an invisible
    cursor through the UI.
  2. Motion-sensitive users get CRT scanlines and pulse regardless of
    their system setting.
    No @media (prefers-reduced-motion: reduce)
    anywhere in app.css. The "intentional" terminal aesthetic
    becomes an accessibility barrier for users with vestibular
    disorders.
  3. InterruptPanel modal backdrop is a non-semantic
    <div role="button" tabindex="-1" onclick=…>
    with a
    svelte-ignore a11y_click_events_have_key_events workaround.
    Screen readers don't announce it; keyboard users can't close it
    by clicking outside.
  4. Icon-only buttons (↑ ↓ ×) in ProviderList and ChannelList
    carry only title="..." for hover tooltips. Screen readers
    announce them as just "button".
  5. transition: all 0.2s ease appears 78 times across 24 files.
    The compositor tracks every animatable property — including
    layout-sensitive ones the components never change on hover — which
    on weaker hardware shows up as visible jank.

What this PR ships

Commit 1 — ui: fix top a11y issues from web-design-guidelines audit

7 files, +79/−40.

  • src/app.css — replaces global outline: none with a
    :focus-visible ring (outline: 2px solid var(--accent) plus
    outline-offset: 2px) covering input, textarea, button and anchor.
    Adds a @media (prefers-reduced-motion: reduce) block that
    neutralises the CRT scanline-drift and pulse animations
    (animation-duration: 0.01ms + explicit animation: none on the
    body pseudo-elements).
  • src/lib/components/orchestration/InterruptPanel.svelte — drops
    the <div role="button"> overlay backdrop in favour of a real
    <button class="backdrop"> so backdrop-click is reachable by
    keyboard. Adds aria-modal="true" on the panel and removes the
    svelte-ignore workaround.
  • src/lib/components/ProviderList.svelte and
    src/lib/components/ChannelList.sveltearia-label on the
    reorder (, ) and remove (×) icon-only buttons, with the
    glyph wrapped in <span aria-hidden="true"> so it isn't
    double-announced.
  • src/lib/components/TopBar.svelte,
    InstanceCard.svelte, ConfigEditor.svelte
    — replace
    transition: all with explicit property lists in the most-visible
    components.

Commit 2 — ui: replace transition: all with explicit property lists

22 files, +64/−64. Mechanical sweep of the remaining 78
transition: all <duration> ease occurrences across components and
routes. Each becomes:

transition:
  background-color  <duration> ease,
  border-color      <duration> ease,
  box-shadow        <duration> ease,
  color             <duration> ease,
  transform         <duration> ease,
  text-shadow       <duration> ease;

Durations preserved as-is (mostly 0.2s, a few 0.15s, one 0.1s).
No behaviour change beyond the perf/jank improvement.

Verification

> nullhub-ui@0.0.0-dev build
> vite build
✓ built in 3.44s

  $ grep -rE 'transition:\s*all' src/
  (no output — 0 matches)

Manual verification in Chrome via agent-browser:

  • Focus ringdocument.activeElement after Tab carries
    outline: rgb(0, 255, 65) solid 2px and the glow box-shadow. The
    ring is clearly visible on the rendered page (screenshot
    02-focus-ring.png in the project's screenshots-audit/).
  • prefers-reduced-motion — with the emulation off,
    body::before has animation-name: crt-scanline-drift and
    animation-duration: 9s. With agent-browser set media reduced-motion, both become none / 0s. The media query is
    load-bearing.
  • Aria-labels in the bundlegrep on the production bundle
    confirms aria-label="Close dialog", Move provider up,
    Move provider down, Remove provider, Remove channel are all
    emitted in their respective node chunks.

The modal itself can't be exercised end-to-end without a real
orchestration interrupt, but the static markup is correct and the
build compiles.

Scope

Out of scope:

  • Visual redesign — the CRT/Matrix aesthetic is preserved. The
    reduced-motion block only fires when the user has explicitly asked
    the OS for less motion.
  • Business logic — no .ts files in lib/api/ or stores
    touched.
  • Performance worktransition: all → explicit is incidentally
    better for the compositor, but this PR doesn't claim a measurable
    perf win, just correctness.

In scope, not done here (worth a follow-up):

  • The audit flagged ~25 more transition: all violations that were
    not in the most-visible components and are covered by the second
    commit, plus several smaller items (missing inputmode on number
    inputs, placeholders not ending in , hardcoded date formats).
    They are real but lower-impact than the top 5.

Affected files

ui/src/app.css
ui/src/lib/components/ChannelList.svelte
ui/src/lib/components/ComponentCard.svelte
ui/src/lib/components/ConfigEditor.svelte
ui/src/lib/components/ConfigEditorUI.svelte
ui/src/lib/components/InstanceCard.svelte
ui/src/lib/components/LogViewer.svelte
ui/src/lib/components/ProviderList.svelte
ui/src/lib/components/Sidebar.svelte
ui/src/lib/components/StructuredConfigEditor.svelte
ui/src/lib/components/TopBar.svelte
ui/src/lib/components/WizardRenderer.svelte
ui/src/lib/components/WizardStep.svelte
ui/src/lib/components/orchestration/CheckpointTimeline.svelte
ui/src/lib/components/orchestration/InterruptPanel.svelte
ui/src/lib/components/orchestration/StateInspector.svelte
ui/src/routes/+page.svelte
ui/src/routes/channels/+page.svelte
ui/src/routes/dashboard/+page.svelte
ui/src/routes/instances/[component]/[name]/+page.svelte
ui/src/routes/orchestration/+page.svelte
ui/src/routes/orchestration/runs/[id]/+page.svelte
ui/src/routes/orchestration/runs/[id]/fork/+page.svelte
ui/src/routes/orchestration/store/+page.svelte
ui/src/routes/orchestration/workflows/+page.svelte
ui/src/routes/orchestration/workflows/[id]/+page.svelte
ui/src/routes/providers/+page.svelte
ui/src/routes/report/+page.svelte
ui/src/routes/settings/+page.svelte

29 files, +143/−104 across the two commits.

Environment

  • nullhub-ui@0.0.0-dev — Vite 6 + SvelteKit 2 + Svelte 5
  • Tested in Chromium 148 via agent-browser
  • npm run build — clean (Windows host, MSYS bash)

Kures added 2 commits May 8, 2026 20:19
Keyboard users had no visible focus, motion-sensitive users got the CRT
drift even with prefers-reduced-motion set, the run-interrupt overlay was
a non-semantic div that needed an a11y-suppress comment, and the
provider/channel reorder/remove buttons were icon-only with no
screen-reader name.

- app.css: replace global `outline: none` with `:focus-visible` ring
  (2px solid accent + 2px offset) covering input/textarea/button/anchor;
  add `@media (prefers-reduced-motion: reduce)` block that neutralises
  CRT scanline + pulse animations.
- InterruptPanel: drop the `<div role="button">` overlay backdrop in
  favour of a real `<button class="backdrop">` so backdrop-click is
  reachable by keyboard; add `aria-modal="true"`; remove the
  `svelte-ignore a11y_click_events_have_key_events` workaround.
- ProviderList / ChannelList: add `aria-label` to the icon-only
  reorder (^/v) and remove (x) buttons; mark the glyph
  `aria-hidden` so it isn't double-announced.
- TopBar / InstanceCard / ConfigEditor / InterruptPanel: replace
  \`transition: all\` with explicit property lists (background-color,
  border-color, box-shadow, color, transform, text-shadow) so the
  compositor isn't asked to animate everything.
`transition: all` forces the browser to track every animatable property,
including layout-sensitive ones (width, height, padding) the components
never actually change on hover/focus. That wastes work on every
interaction and on weaker hardware shows up as visible jank during the
hover/transition. Listing the properties we actually animate
(background-color, border-color, box-shadow, color, transform,
text-shadow) lets the compositor skip everything else.

Mechanical sweep across the remaining 22 files; the previously-touched
TopBar / InstanceCard / ConfigEditor / InterruptPanel were already
converted in the prior a11y commit. Durations preserved as-is (mostly
0.2s ease, a few 0.15s, one 0.1s).

No behaviour change beyond the perf/jank improvement.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant