Skip to content

fix(agent): name a form control from the label that names it - #55

Merged
pathscale merged 8 commits into
masterfrom
fix/accessible-name-from-label
Sep 9, 2026
Merged

pathscale merged 8 commits into
masterfrom
fix/accessible-name-from-label

Conversation

@pathscale

Copy link
Copy Markdown
Owner

A semantic name came from aria-label, alt and title and from nothing else. The ordinary way to label a form control — a <label for> beside it, or a <label> wrapped around it — produced no name at all, so every text input on every page in the fleet arrived in the tree anonymous.

That is not only a reporting defect. A harness addresses a control by name, so an anonymous field cannot be typed into and a check that means "enter a URL and save" cannot be written at all.

Measured

A probe page through the headless host, before:

markup name
<label for=a>Endpoint URL</label><input id=a> ""
<label>Wrapping <input></label> ""
<input aria-label="Direct aria"> Direct aria
<input placeholder="Only a placeholder"> ""

And on support.cafe's connection settings: three Input.Fields, each with a correct <Label for> beside it, all three reported as textbox "".

Resolution order

A browser's: aria-label first, because that is the author overriding the visible text on purpose; then the label, for before containment; then alt and title; then the text content of the roles named by their own content; and a placeholder last, which is the only thing naming a great many search fields and must never displace a real label.

Cost

The index is built once per snapshot. The question a control asks is "which label points at me", and answering that from the control means scanning the document for every named node. It is built over the whole document even for a rooted inspection, because a label is frequently a sibling of the control rather than a descendant of the node the caller rooted at.

a_field_is_named_by_its_label pins all five cases, including the two that must not change: aria-label still wins over a label, and a control nothing names still has no name.

🤖 Generated with Claude Code

meh and others added 2 commits September 8, 2026 13:23
A semantic name came from `aria-label`, `alt` and `title` and from nothing
else, so the ordinary way to label a field -- a `<label for>` beside it, or a
`<label>` wrapped around it -- produced no name at all. Every text input on
every page in the fleet arrived in the tree anonymous.

That is not only a reporting defect. A harness addresses a control by name,
so an anonymous field cannot be typed into, and a check that means "enter a
URL and save" cannot be written at all. Measured on support.cafe's connection
settings: three fields, each with a correct `<Label for>` beside it, all three
reported as `textbox ""`. Measured against a probe page as well, where a
`for` association, a wrapping label and a placeholder all produced nothing and
only `aria-label` worked.

Resolution order follows a browser's: `aria-label` first, because that is the
author overriding the visible text on purpose; then the label, `for` before
containment; then `alt` and `title`; then the text content of the roles that
are named by their own content; and a placeholder last, which is the only
thing naming a great many search fields and must never displace a real label.

The index is built once per snapshot. The question a control asks is "which
label points at me", and answering that from the control means scanning the
document for every named node. It is built over the whole document even for a
rooted inspection, because a label is frequently a sibling of the control
rather than a descendant of the node the caller rooted at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`alert` and `status` are the roles an application uses to report that
something happened -- a refusal, a saved confirmation -- and what they report
is their content. They arrived anonymous, so "the reason is shown" was not a
question a check could ask, and every validation outcome had to be
approximated by something else that moved at the same time.

Found writing checks for support.cafe's connection settings, where the
refusal and the success message are both bare paragraphs. Marking them as
live regions is the accessibility fix on the site's side; naming them is this
side of it.

Deliberately not `generic`. A wrapper's text content is its whole subtree, so
naming containers would give every one of them a name made of the entire
page, and any name-matching selector would then match everything. The test
pins that half too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
meh and others added 6 commits September 8, 2026 14:40
A version bump on master is the release trigger, and this is what carries the
naming fixes to the harness that needs them: without them every form field in
the fleet arrives anonymous and cannot be addressed by name at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`textContent` is the DOM property and includes every text node in the
subtree, `<style>` and `<script>` among them. An accessible name does
not, because those elements are not rendered.

Measured on honey.id, whose header logo is an anchor wrapping an inline
SVG with a `<style>` in it. The site's home link arrived named
".animated-logo path { fill-opacity: 0; stroke: currentColor; ... }",
which is unusable to a person reading the page and unaddressable to a
check trying to press it. Any site whose logo is an inline SVG with
scoped styles has the same link.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An alert stacking two lines arrived named "This page could not
loadWebSocket connection failed", because every text node under the
element was concatenated with nothing between it and the next. A name
that reads as one run-on word is one a person cannot read and a check
cannot address.

accname separates each descendant's contribution with a space unless
the descendant is inline, which is why two adjacent spans are still one
word. The walk now asks the resolved style which of the two a child is,
and treats an element with no style as inline so a name never gains a
space the page does not draw.

Measured on crates.vip's failure alert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`inline-block` and `inline-flex` establish their own box, so a browser
separates them from their siblings the way it separates a block. The
first pass asked only whether the display was outside-inline, which put
an inline-block back in the same word as its neighbour.

Checked against `dom-accessibility-api`, which reaches the same result
by comparing the computed display against the string "inline", and
against the pair of names crates.vip's navbar and its failure alert
produce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "named by their own content" list had `option` but not its menu, tab and
tree equivalents, and `semantic_role` returns the `role` attribute verbatim. So
`<button role="menuitem">Platform Admin</button>` arrived with an empty name:
a screen reader announces nothing, and no check can name the option it means to
press. Writing the role is the author opting out of the native naming this list
exists to provide, which is exactly when it is needed.

Reduced from honey.id, where the role selector on the platform users table
offered two anonymous entries and promoting a user could not be driven or
described. It is not specific to that dropdown: every `Dropdown.Item` in
@pathscale/ui carries `role="menuitem"`, so the whole fleet was affected.

Confirmed against a minimal fixture, where a plain `<button>` and an `<a>` were
named and all three of `<button role="menuitem">`, the same wrapped in a span,
and `<div role="menuitem">` were not. All three are named now.

`cell` and `row` stay off the list. Their content is a whole subtree, which is
the objection the comment above already raises against `generic`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`name_for` returned the first label it found, empty or not, and stopped. Labels
nested inside labels are invalid markup but they happen: a checkbox component that
draws its own `<label>` around a styled box, wrapped again by the page to put the
text beside it. The inner label's content is the box, so it is empty, and
returning that emptiness made the control anonymous while its name sat one level
further out.

Skipping an empty label costs nothing when the markup is well formed, because a
real label has text. The same guard on the `for` lookup, for the same reason.

Found in honey.id, where the sign-up form's accept-terms checkbox came back with
no name and nothing announced what was being accepted. That page is fixed at
source too -- the text belongs to the checkbox -- but a control should not go
anonymous because the markup around it is careless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pathscale
pathscale merged commit d8269ed into master Sep 9, 2026
3 checks passed
@pathscale
pathscale deleted the fix/accessible-name-from-label branch September 9, 2026 07:35
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