hermes - 💡(How to fix) Fix a11y: dashboard renders ~300 form fields per page without programmatic labels (audit data + reproduction + fix shape)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

A Lighthouse / axe-core audit of the dashboard at http://127.0.0.1:9119/ reports 300+ unlabeled form fields per page load on /config alone after the standard category sections expand. The fields render correctly visually but lack the programmatic associations screen readers need: missing id+name, <label for=X> pointing at non-existent IDs, dangling <label> elements that wrap or sit next to <button role="switch"> (shadcn-style) widgets without an aria-labelledby link, and orphan inputs with no label of any kind.

This complements #25915 (broader a11y contract proposal) and #26689 (VoiceOver-focused improvements) with a concrete inventory + fix shape.

Root Cause

Root cause (best-effort, based on observed DOM structure)

Fix Action

Fix / Workaround

We patched a runtime auto-fix layer onto our local install for triage and to keep the dashboard usable in the meantime. Its log (after expanding every category section on /config) reports:

This audit was conducted with the assistance of an AI agent (Anthropic, claude-opus-4-7) running a runtime DOM-instrumentation patch we ship for our own use. The reproduction script above is a sanitized version of that instrumentation. The fix-shape recommendation is the agent's translation of observed patterns — please treat as a starting point for your team's actual design.

Code Example

(function () {
  var noLabel = 0, brokenFor = 0, missingIdName = 0;
  document.querySelectorAll('input, select, textarea').forEach(function (el) {
    var t = (el.getAttribute('type') || '').toLowerCase();
    if (['hidden', 'submit', 'button', 'reset', 'image'].indexOf(t) >= 0) return;
    var id = el.getAttribute('id');
    var hasLabelFor = id && document.querySelector('label[for="' + id.replace(/"/g, '\\"') + '"]');
    var ariaL = el.getAttribute('aria-label') || el.getAttribute('aria-labelledby');
    var p = el.parentElement, wrapped = false;
    while (p && p !== document.body) { if (p.tagName === 'LABEL') { wrapped = true; break; } p = p.parentElement; }
    if (!hasLabelFor && !ariaL && !wrapped) noLabel++;
    if (!id && !el.name) missingIdName++;
  });
  document.querySelectorAll('label[for]').forEach(function (l) {
    if (!document.getElementById(l.getAttribute('for'))) brokenFor++;
  });
  console.log({ noLabel: noLabel, brokenFor: brokenFor, missingIdName: missingIdName });
})();

---

{ noLabel: 130, brokenFor: 197, missingIdName: 127 }

---

<button type="button" role="switch" aria-checked="false"></button>

---

<div class="flex flex-col gap-0.5">
  <label class="font-mondwest …">Hooks Auto Accept</label>
  <button type="button" role="switch" aria-checked="false"></button>
</div>

---

<div class="flex flex-col gap-0.5">
  <label class="font-mondwest …" htmlFor="hooks-auto-accept-switch">Hooks Auto Accept</label>
  <button type="button" role="switch" aria-checked="false"
          id="hooks-auto-accept-switch"
          aria-labelledby="hooks-auto-accept-label"></button>
</div>
RAW_BUFFERClick to expand / collapse

Summary

A Lighthouse / axe-core audit of the dashboard at http://127.0.0.1:9119/ reports 300+ unlabeled form fields per page load on /config alone after the standard category sections expand. The fields render correctly visually but lack the programmatic associations screen readers need: missing id+name, <label for=X> pointing at non-existent IDs, dangling <label> elements that wrap or sit next to <button role="switch"> (shadcn-style) widgets without an aria-labelledby link, and orphan inputs with no label of any kind.

This complements #25915 (broader a11y contract proposal) and #26689 (VoiceOver-focused improvements) with a concrete inventory + fix shape.

Reproduction

  1. Run hermes (hermes then hermes ui) or python3 hermes_cli/web.py
  2. Open http://127.0.0.1:9119/config in Chrome
  3. Click each category button (General, Agent, Delegation, Memory, Security, etc.) to expand its toggle list
  4. Open DevTools → Lighthouse → "Accessibility" only → "Analyze page load"
  5. Observe warnings under:
    • "Form elements should have associated labels"
    • "<label> elements should be programmatically associated with form fields"
    • "Form field elements should have an id or name attribute"
    • "Incorrect use of <label for=FORM_ELEMENT>"

Also reproducible by pasting into the DevTools console after the page is fully rendered:

(function () {
  var noLabel = 0, brokenFor = 0, missingIdName = 0;
  document.querySelectorAll('input, select, textarea').forEach(function (el) {
    var t = (el.getAttribute('type') || '').toLowerCase();
    if (['hidden', 'submit', 'button', 'reset', 'image'].indexOf(t) >= 0) return;
    var id = el.getAttribute('id');
    var hasLabelFor = id && document.querySelector('label[for="' + id.replace(/"/g, '\\"') + '"]');
    var ariaL = el.getAttribute('aria-label') || el.getAttribute('aria-labelledby');
    var p = el.parentElement, wrapped = false;
    while (p && p !== document.body) { if (p.tagName === 'LABEL') { wrapped = true; break; } p = p.parentElement; }
    if (!hasLabelFor && !ariaL && !wrapped) noLabel++;
    if (!id && !el.name) missingIdName++;
  });
  document.querySelectorAll('label[for]').forEach(function (l) {
    if (!document.getElementById(l.getAttribute('for'))) brokenFor++;
  });
  console.log({ noLabel: noLabel, brokenFor: brokenFor, missingIdName: missingIdName });
})();

On a fresh /config page with all categories expanded, this prints something close to:

{ noLabel: 130, brokenFor: 197, missingIdName: 127 }

(Exact numbers vary slightly with config items present; the orders of magnitude are stable.)

Root cause (best-effort, based on observed DOM structure)

The dashboard appears to use a <Switch> component (Radix/shadcn-style, possibly via your own UI library) that renders to:

<button type="button" role="switch" aria-checked="false"></button>

A neighboring <label> element provides the human-readable text. But the two are not linked via htmlFor+id and the button has no aria-labelledby pointing at the label. From the assistive-tech perspective, the switch has no name.

A simplified observed pattern:

<div class="flex flex-col gap-0.5">
  <label class="font-mondwest …">Hooks Auto Accept</label>
  <button type="button" role="switch" aria-checked="false"></button>
</div>

The fix shape (one of several valid options):

<div class="flex flex-col gap-0.5">
  <label class="font-mondwest …" htmlFor="hooks-auto-accept-switch">Hooks Auto Accept</label>
  <button type="button" role="switch" aria-checked="false"
          id="hooks-auto-accept-switch"
          aria-labelledby="hooks-auto-accept-label"></button>
</div>

htmlFor alone gets axe-core to recognize the association; aria-labelledby is what screen readers actually consume. Belt-and-suspenders.

The exact same pattern repeats for many other categories (Delegation, Memory, Security, Compression, Browser, Voice, Logging, Discord, Auxiliary, Bedrock, Curator, Kanban, Lsp, Matrix, Mattermost, Openrouter, Secrets, Sessions, Slack, Updates, Web).

Categorized counts (from our audit instrumentation)

We patched a runtime auto-fix layer onto our local install for triage and to keep the dashboard usable in the meantime. Its log (after expanding every category section on /config) reports:

Lighthouse warning classCount
Class 1: input/textarea missing id+name (fixed at runtime via synthesized id+name)127
Class 2: <label> with no for and no nested control (re-anchored to nearest control via htmlFor + aria-labelledby)197
Class 3: <label for="X"> where no id="X" exists (re-anchored)0 (after fix)
Class 4: orphan input with no label/aria-label (synthesized aria-label)2
Dangling labels with no associated control of any kind0 (after Class 2's ARIA-role widget extension)
Total fixes applied per /config load~326

The "Class 2" jump (from 19 to 197 in our instrumentation) was the surprise — it happened when we extended our control search to include ARIA-role widgets ([role="switch"], [role="checkbox"], [role="radio"], etc.). That delta is the inventory of currently-unlabeled <Switch>/<Checkbox> instances.

/sessions (a sparser page) hits ~1-2 fixes. /skills hits ~2. /config is the highest-volume page; the others follow the same pattern at lower scale.

Affected routes (observed)

  • /config — all category sections (largest concentration)
  • /sessions — sidebar search input (1 missing label)
  • /skills — search input (1)
  • /kanban — confirmed by #23620 which is the same class of bug at a single call site

Suggested fix shape (source-level)

  1. Tighten the <Switch> / <Checkbox> / <Radio> components themselves to require an id prop AND to emit aria-labelledby if a sibling <Label> declares htmlFor matching that id. One change, many sites benefit.
  2. Add an axe-core / Lighthouse CI gate on /config so this regression class doesn't reappear after future component changes.
  3. Audit the config-key rendering for any direct <button role="..."> that should instead use the shared component.
  4. For sidebar searches (/sessions, /skills, /kanban), add explicit aria-label props matching the placeholder text.

If maintainers want, we can draft these as separate PRs by component category (Switch, Checkbox, Radio, Combobox, etc.) so each is reviewable in isolation. That sequencing is in our backlog as a follow-up to filing this issue.

What this issue explicitly is NOT proposing

  • Replacing the existing component library. The fix is internal to whatever component library you already use.
  • A specific naming convention for ids. Whatever scheme matches your existing codebase. The audit just needs ids to exist.
  • Visual changes. The fix is pure ARIA / DOM attribute additions. No layout or theming impact.

AI Usage Disclosure

This audit was conducted with the assistance of an AI agent (Anthropic, claude-opus-4-7) running a runtime DOM-instrumentation patch we ship for our own use. The reproduction script above is a sanitized version of that instrumentation. The fix-shape recommendation is the agent's translation of observed patterns — please treat as a starting point for your team's actual design.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING