ADR 0214: A captioned control is named by its caption
Status: Accepted
Context
Section titled “Context”A field with a visible caption (“Email address”) connects the caption to its control, so a screen
reader announces the caption as the control’s name. On top of that connection, three renderers write
an aria-label on the control, copied from the caption, even when the document declared no
ariaLabel. The fourth writes aria-label only when the document declares one. A browser spec that
compares which aria-* attributes each renderer writes reports ten such rows. The contract has no
general rule to settle them.
The repository already leans one way in three places:
- the name-source comment in
packages/widgets/src/field/verdict.tssays that, for a control a<label for>can name, a declared name is the extra thing to say and goes on asaria-label; fieldNameAttributesgivesaria-label: nullwhenever a caption has words, although it is scoped to composites;- ADR 0177 treats “the caption’s reference or its words” as
one answer, not two, but only for
file.
The only stated argument for copying the caption is in the plain shell. The caption element also
holds the visible required marker, *, so a name computed from the caption’s content carries an
asterisk the caption’s words do not.
The outside view (accname 1.2 steps 2B to 2E; WCAG 2.2 SC 2.5.3 Label in Name and its Understanding
document) is unambiguous. aria-label does not add to a native label, it replaces it. When a visible
caption exists, the name must come from that caption. A second copy is harmless only while the two
are identical, and they drift the first time one is edited. Extra words for assistive technology
belong in the description, not the name.
Decision
Section titled “Decision”A control whose field has a caption with words is named by that caption, through the native label
connection or aria-labelledby. The renderer writes no aria-label derived from the caption.
aria-label is written only when the document declares ariaLabel. A declared name is the
document’s explicit choice and is not rewritten. The renderer neither prefixes it with the caption
nor drops it. A field with no caption words keeps the existing fallback order in
fieldAccessibleName: declared ariaLabel, then the caption, then the field’s name.
The required marker is not part of the accessible name. It is decoration on the caption, and
aria-required already says that the field is required. The marker is therefore hidden from
assistive technology, and the caption’s words alone form the name.
Consequences
Section titled “Consequences”- Plain, Lit and Angular must stop copying the caption into
aria-label, and every renderer must hide the required marker from the name. Those changes cross three adapters and are a separate batch. Until they land, the spec stays on the known-red list. Ten of the fourteen rows it reports arearia-labelrows and belong to this decision. The other four (aria-liveon the select’s error region twice,aria-modalon the timepicker popup,aria-readonlyon the colors hex input) are outside it, and no record decides them yet. - The asterisk argument is answered at its source, the marker, rather than by a second naming channel. A theme or renderer that puts other decoration inside the caption takes on the same obligation to hide it.
- A declared
ariaLabelthat does not contain the caption’s words fails SC 2.5.3. The library allows this because the declaration is the document’s own. A warning for it would be a separate, additive decision. - An announced name can change for a captioned, required field that today reads “Email address *”. That change is visible to screen reader users and to tests that match names exactly.
Alternatives rejected
Section titled “Alternatives rejected”- Keep copying the caption into
aria-label. Two sources of truth for one name. When they diverge, the hidden one wins under accname and the visible caption silently stops naming the control. This is the failure SC 2.5.3 exists to prevent. - Have every renderer copy it, so the four agree. This buys agreement by adding the redundant channel everywhere, and makes it public surface 1.0 would have to keep.
- Leave it to each renderer. The spec exists because a person hears different things from the same document depending on the toolkit. ADR 0177’s test for an optional choice (“two renderings would tell a person different things”) rules this out.
Verification
Section titled “Verification”battle-tests/browser/an-aria-promise-that-depends-on-the-adapter.spec.ts fails while any renderer
writes aria-label on a captioned field that some other renderer leaves off. Today it reports fourteen
rows, ten of them aria-label, and battle-tests/reports/known-red-browser.json lists it once. The
adapter batch clears the ten. The spec leaves the list, and guards this rule, only once the other
four are decided as well. No check yet asserts that the required marker is
outside the name. That check belongs with the adapter batch.
Amendment, 2026-10-10 (adapter batch). The rule is one export, fieldAriaLabel in
@modyra/widgets. Plain, Lit and Angular call it instead of copying the caption, and its unit test is
in packages/widgets/test/form-shell.spec.mjs. The contract declares the marker hidden: projectFieldShellA11y and
projectBooleanFieldA11y return a requiredMarker part carrying aria-hidden="true", in the same
way the boolean’s drawn indicator is declared. Every renderer writes the attribute where it draws
the mark. Lit and Angular already wrote it, and Plain and Vue now do too. The declaration was chosen
over leaving the rule to each renderer (TypeSafe, 76%), so that the next renderer reads the obligation
from the contract instead of copying it from a sibling. battle-tests/browser/a-name-the-required-mark-stays-out-of.spec.ts
reads the computed name of a required, captioned control in every renderer and fails if the mark
enters it. After the batch, the aria spec reports no aria-label row that copies a caption. Two
aria-label rows remain, and this decision does not own them. One is the number’s step buttons,
which Vue does not draw. The other is the select’s search box: the contract names it through
MDY_PART_NAMES, and only Lit writes that name.
Security and privacy
Section titled “Security and privacy”None. The decision changes which attribute carries an accessible name. No data, storage or trust boundary is involved.