Repository navigation
fix(bulma-ui): name custom form controls from a Field label - #974
Conversation
A labeled Field gave its id only to InputBase, SelectBase and TextAreaBase. The other convenience inputs drop their own label inside a Field and took nothing in its place, so Numberinput, Slider, DateInput, TimeInput, DateTimeInput and the Radios and Checkboxes groups had no accessible name there, and Autocomplete, Taginput, File and Rate fell back to a placeholder or a built-in name. useAutoLabelId now adopts the surrounding Field's id when the control renders no label of its own, so each single-input control puts it on its real input. Field gives its label an id while the association is on, and useAutoLabelledBy points a group's aria-labelledby at it. An id the caller set on the control still wins, as does an aria-label or aria-labelledby on a group, and an inline picker, which has no input, takes nothing. The Field accessibility notes, each control's TSDoc and its Context-Aware section say so. The horizontal examples there put the control in a nested Field, which starts its own scope, so they now wire the label by hand. The basic Numberinput and Slider examples get a label. Fixes #939
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 42 minutes. View limit detailsLimit details: You’ve used the included review currently available. Review configuration: ⚙️ Run configuration
📒 Files selected for processing (58)
WalkthroughField labels now provide accessible names to eligible single controls and groups. The change adds label context and hook logic, updates form controls and tests, and revises component examples and API guidance. ChangesField label association
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Bug fix · Severity of issue fixed: Medium Sequence Diagram(s)sequenceDiagram
participant Field
participant FormContext
participant LabelHooks as useAutoLabelId and useAutoLabelledBy
participant Controls as Single controls and groups
Field->>FormContext: Provide label target and label element ID
FormContext->>LabelHooks: Supply Field label context
LabelHooks->>Controls: Resolve control ID or aria-labelledby
Fixed issue severity: Medium Merge Risk: 🟡 Moderate · up to Groups inside labeled Fields can lose their accessible names when a wrapper forwards an undefined aria-labelledby value. Preserve the Field reference in that case before merging. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Preview DeploymentPreview URL: https://9e42bb3c.bestax.pages.dev |
|
deep-review: fresh The last run on 91c9b42 was cancelled at the runner limit after it posted four threads (on |
At its tag limit a Taginput renders no input, yet it still adopted a surrounding Field's id, against the convention the label hooks set for a mode with nothing to label. It now passes hasInput the way the inline pickers do, and their guard test covers it too. Nothing renders differently today; this keeps a future id derived from the control from colliding. The Field docs no longer say the component pages' horizontal examples are all wired by hand, since the Input, Select and TextArea pages aren't yet (#968).
|
deep-review: fresh Two runs on 91c9b42 were cancelled at the runner limit before posting a summary. Between them they posted six threads, on |
… hand
A Taginput dropped its "Add tag" aria-label only when an id its Field generated tied a label to
it. With labelProps={{ htmlFor }} on the Field and a matching id, the pattern the horizontal
examples teach, the fallback stayed and outranked the label, so the input still announced
"Add tag". Field now passes down what its label's for actually points at, and an unlabeled Field
passes on its parent's, so the hook sees a hand-wired label across the inner Field too. It only
compares that target with the control's own id and never hands it out.
There was a problem hiding this comment.
Deep review — 4 blocking · 4 advisory
| # | Severity | Area | Finding | Location |
|---|---|---|---|---|
| 1 | 🟠 Major | Accessibility | The labelProps={{ htmlFor }} + id pattern the docs and skill now prescribe cannot name a Taginput — labelled is false, so aria-label="Add tag" stays and outranks the label[for] |
bulma-ui/src/form/useAutoLabelId.ts:70 |
| 2 | 🟡 Minor | Accessibility | Two pickers in one labeled Field now collide on aria-controls: popoverId derives from the shared adopted id, where it used to be per-instance |
bulma-ui/src/form/useAutoLabelId.ts:58 |
| 3 | 🟡 Minor | Accessibility | A caller aria-labelledby={undefined} is falsy for callerNamed yet still wins the spread, so it cancels the Field association — and Rate loses its "Rating" fallback with it, going from named to nameless |
bulma-ui/src/form/useAutoLabelId.ts:124 |
| 4 | 🟡 Minor | Accessibility | Six of the nine newly-named controls (Numberinput, both Slider thumbs, File, the three pickers) lose the adopted id to an own-but-undefined id key, because id={controlId} sits before the props spread |
bulma-ui/src/form/useAutoLabelId.ts:59 |
| 5 | 🔵 Advisory | Accessibility | A range Slider inside a labeled Field is still not named by it: both thumbs keep an unconditional aria-label, which outranks the label[for] the low thumb now resolves |
bulma-ui/src/form/Slider.tsx:61 |
| 6 | 🔵 Advisory | Accessibility | <Field label><File/></Field> now gives the file input two associated labels (the Field's plus the wrapping file-label), which is what form-field-multiple-labels fires on |
bulma-ui/src/form/File.tsx:35 |
| 7 | 🔵 Advisory | Robustness | Taginput at maxTags adopts the Field's id with no input to put it on — it is the one single-input control that renders none in a reachable mode yet does not pass hasInput: false |
bulma-ui/src/form/Taginput.tsx:281 |
| 8 | 🔵 Advisory | Accessibility | "as the component pages' horizontal examples do" is not true of input.md, select.md or textarea.md, whose six horizontal examples still render an unnamed control |
docs/docs/api/form/field.md:495 |
Overall: The core design is sound and lands #939 properly: useAutoLabelId adopting the Field's id when the control renders no label of its own, plus a second context carrying the label element's own id for the three groups, is the minimal shape that reaches every control, and the precedence rules (caller id wins, caller ARIA wins, grouped/hasAddons/nested-Field opt out, inline pickers take nothing) are all pinned by tests. pnpm --filter @allxsmith/bestax-bulma exec jest src/form passes (42 suites, 2496 tests) on this branch, and gen:mcp:check and check:conformance are both green, so the generated index and props tables match their sources. The riskiest part is not the hook but the ordering at the call sites: three of the four blocking findings are the same root shape — an attribute the new code computes is written before a props spread that can carry the same key with an undefined value, which silently wins. InputBase.tsx:95-98 already carries a comment naming that exact hazard, so the pattern to copy is in the repo. Start a human review with findings 3 and 4 (they decide whether the fix actually reaches the six controls that matter most), then finding 1, which is the one case where the escape hatch the new docs and the shipped bestax-form skill prescribe produces a wrong name rather than no name.
Residual risk:
- Controls still nameless inside a labeled Field. Refuted for the eleven the PR covers by the new
it.each(singles)/it.each(groups)matrices inField.test.tsx, which assert the accessible name (getByLabelText/getByRole(role, { name })), not just a matchingfor— except through findings 3 and 4, which are posted.Checkbox/Switchand inline pickers take no association by design andfield.md:497says so. - Ids derived from the adopted id. Swept
src/form/*.tsxfor${id}-templates: only the three pickers'popoverIdderives one, which is finding 2.Field's own${targetId}-labelcannot collide with it, and theleaves an inline picker alonetest pins thehasInput: falsepath through[id^="${target}"]:not(label). - Duplicate ids from two controls in one Field. Open and accepted:
field.md:496and thelabelTSDoc tell the caller to give each control anidof its own. Nothing enforces it, and finding 2 is the sharper consequence of ignoring it. - The nested-
Fieldscope reset. Refuted:Fieldre-provides both contexts with its owntargetId, so an innerFieldresets them, anda nested unlabeled Field shadows the outer label for groups toopins it for the new context as well as the old. hasInput: falsecoverage. Open, low: onlyDateInput inlineis exercised inside a labeledField;TimeInput/DateTimeInputinline ride the same shared hook, so the exposure is a future divergence rather than a defect today.- Hydration. Refuted: the label's
idand every adopted id come fromuseIdthrough the same render path as #495's, so server and client agree; there is no clock or random input anywhere in the diff.
🏄 Clean set, honestly — paddled out and actually caught the wave #939 was pointing at, with eleven controls finally announcing their own names instead of mumbling "Add tag". Just watch the shorebreak where
{...rest}crashes over the id you just set: tidy that up and this one rides all the way in.
|
deep-review: verify Every open thread has a reply: fixed, with the commit, or refuted with a reason. This settles them on ddb7c34. |
Preview DeploymentPreview URL: https://fe066531.bestax.pages.dev |
There was a problem hiding this comment.
Deep review (verify) — 4 resolved · 0 open
| # | Severity | Area | Thread | Location | Outcome |
|---|---|---|---|---|---|
| 1 | 🟡 Minor | API | "and the like" over-claims: Switch/Checkbox/Radio adopt nothing |
bulma-ui/src/form/Field.tsx:33 |
✅ Verified fixed |
| 2 | 🔵 Advisory | Accessibility | Listbox name degrades to "Suggestions" inside a labeled Field |
bulma-ui/src/form/Autocomplete.tsx:45 |
✅ Accepted — tracked in #998 |
| 3 | 🔵 Advisory | API | Caller aria-label beat a Field's label but lost to the group's own label |
bulma-ui/src/form/useAutoLabelId.ts:138 |
✅ Verified fixed |
| 4 | 🔵 Advisory | Accessibility | Six group examples emit a for that matches nothing |
docs/docs/api/form/checkboxes.md:83 |
✅ Verified fixed (+ Field itself tracked in #1004) |
Overall: This pass settled threads and reviewed no commits. All four of my open threads are now resolved: two on code/docs the author fixed in c68fb50a and 4c319bb2 (regenerated in 3b940c78), and two refutations that hold — #998 scopes the Autocomplete listbox name and names this PR as its blocker, and #1004 scopes Field's unmatched for along with the SSR question it raises. The human should focus on the callerNamed reordering in useAutoLabelId.ts:137-142, since it changes #494's own-label precedence for all four groups; the new it.each rows covering both aria props against each group's own label are the evidence that it landed intentionally.
Residual risk: not assessed — a verify pass settles the prior review's findings and raises nothing new. Ask for deep-review: fresh to have the current code reviewed.
What I checked
Field.tsx:33— hedge gone, full adopting set enumerated,Checkbox/Radio/Switchcalled out as taking nothing; propagated todocs/docs/api/form/field.md:528andbestax-mcp/data/components/Field.json:152.useAutoLabelId.ts:137-142—callerNamednow gates outside theactiveternary, exactly the shape suggested.checkboxes.md:86/:114,radios.md:88/:116,rate.md:220/:244— all six carrylabelProps={{ id, htmlFor: undefined }}; nolabelPropswith anidand nohtmlForremains in those pages ordaterangeinput.md.Field.test.tsx:775-784(the three take nothing, still named by children),:870-895(both aria props vs. ownlabel, per group),:898-910(Rate keeps "Rating" on a forwardedundefined),:978-999(hand-wired recipe: id kept, nofor, group named).gh api .../issues/998and.../1004— both open, both scoping the deferred work.pnpm --filter @allxsmith/bestax-bulma exec jest src/form— 46 suites, 2811 tests, all passing.
🏄 Four threads paddled out, four came back in clean — two real fixes, two honest "that's a different wave, already caught it in #998 and #1004." No chop left on this one, it's good to go.
|
🎉 This PR is included in version 5.27.2 🎉 The release is available on: Your semantic-release bot 📦🚀 |
|
🎉 This PR is included in version 1.14.2 🎉 The release is available on: Your semantic-release bot 📦🚀 |
… range Slider thumbs An `aria-label` or `aria-labelledby` passed to a range Slider reached only the low thumb, through the rest spread, so it renamed that thumb while the high one kept the label's name: `<Slider range label="Price" aria-label="Budget floor" />` read "Budget floor" and "Price Maximum value", two unrelated names for one widget. The Slider now routes those two attributes itself instead of spreading them. Either one names the Slider in the label's place, so both thumbs read it before their own text: "Budget Minimum value" and "Budget Maximum value". An `aria-labelledby` reaches the thumbs the way the label does, through `aria-labelledby` and the hidden span, and an `aria-label` is joined to each thumb's text. A non-empty `aria-labelledby` wins over an `aria-label`, as it does in the browser. An `ariaLabel` entry is still its thumb's whole name and wins over both. The presence rule from groups (#974) carries over: any `aria-labelledby` key, even an undefined one, takes the naming over, so `aria-labelledby={undefined}` keeps every label out and leaves both thumbs "Minimum value" and "Maximum value". An empty `aria-label` still counts as unset. Single-thumb Sliders keep spreading the caller's attributes as before.
The range Slider decides whether the caller named it the way a group does (#974): any `aria-labelledby` key counts, even an undefined one, while an `aria-label` counts only when it is non-empty. Every test so far passed a defined `aria-labelledby`, which a plain truthiness check satisfies just as well, so swapping the presence test for one would have kept the suite green while dropping the opt-out. Pin both sides. `aria-labelledby={undefined}`, under the Slider's own label and under a surrounding Field's, leaves both thumbs "Minimum value" and "Maximum value" with no `aria-labelledby` and no hidden spans, while the label's `for` still lands on the low thumb. An empty `aria-label` leaves the label in both names.
|
🎉 This PR is included in version 2.25.2 🎉 The release is available on: Your semantic-release bot 📦🚀 |
… range Slider thumbs An `aria-label` or `aria-labelledby` passed to a range Slider reached only the low thumb, through the rest spread, so it renamed that thumb while the high one kept the label's name: `<Slider range label="Price" aria-label="Budget floor" />` read "Budget floor" and "Price Maximum value", two unrelated names for one widget. The Slider now routes those two attributes itself instead of spreading them. Either one names the Slider in the label's place, so both thumbs read it before their own text: "Budget Minimum value" and "Budget Maximum value". An `aria-labelledby` reaches the thumbs the way the label does, through `aria-labelledby` and the hidden span, and an `aria-label` is joined to each thumb's text. A non-empty `aria-labelledby` wins over an `aria-label`, as it does in the browser. An `ariaLabel` entry is still its thumb's whole name and wins over both. The presence rule from groups (#974) carries over: any `aria-labelledby` key, even an undefined one, takes the naming over, so `aria-labelledby={undefined}` keeps every label out and leaves both thumbs "Minimum value" and "Maximum value". An empty `aria-label` still counts as unset. Single-thumb Sliders keep spreading the caller's attributes as before.
The range Slider decides whether the caller named it the way a group does (#974): any `aria-labelledby` key counts, even an undefined one, while an `aria-label` counts only when it is non-empty. Every test so far passed a defined `aria-labelledby`, which a plain truthiness check satisfies just as well, so swapping the presence test for one would have kept the suite green while dropping the opt-out. Pin both sides. `aria-labelledby={undefined}`, under the Slider's own label and under a surrounding Field's, leaves both thumbs "Minimum value" and "Maximum value" with no `aria-labelledby` and no hidden spans, while the label's `for` still lands on the low thumb. An empty `aria-label` leaves the label in both names.
## [5.27.4](https://github.com/allxsmith/bestax/compare/@allxsmith/bestax-bulma@5.27.3...@allxsmith/bestax-bulma@5.27.4) (2026-10-09) ### Bug Fixes * **bulma-ui:** let a caller's aria-label or aria-labelledby name both range Slider thumbs ([047ff7c](047ff7c)), closes [#974](#974) * **bulma-ui:** name each range Slider thumb from its label ([6b962e3](6b962e3)), closes [#981](#981) * **create-bestax:** announce the starter's notifications through a status region ([91dbfd5](91dbfd5)) * **create-bestax:** give each starter notification a status region of its own ([f1ef4df](f1ef4df)) * **create-bestax:** keep focus on the counter when Reset disables itself ([733e505](733e505)) * **create-bestax:** keep the reason for the stylesheet order in every scaffolded entry file ([f4591e0](f4591e0)) * **create-bestax:** return focus when the starter's notification closes, and tighten guards ([67e3678](67e3678)) * **create-bestax:** say which helper props render nothing under the no-helpers flavors ([b29985b](b29985b)) * **create-bestax:** stop the no-helpers CLAUDE.md offering helper props as the way out ([a96c203](a96c203)) * **create-bestax:** stop the vite-ts build shadowing its config, and fix the starter page ([1dd964d](1dd964d))
## [4.2.15](https://github.com/allxsmith/bestax/compare/create-bestax@4.2.14...create-bestax@4.2.15) (2026-10-09) ### Bug Fixes * **bestax-mcp:** document Theme's variable props, point at the prose page, name the title rule ([e72aee0](e72aee0)) * **bestax-mcp:** give Theme, ConfigProvider, Portal and ClientOnly prop tables ([fddfd65](fddfd65)) * **bestax-mcp:** make the links in markdown the index ships absolute ([fd134fa](fd134fa)) * **bestax-mcp:** mark is-empty rendered by File in its lookup row, not kept as a class ([e53ee6c](e53ee6c)) * **bestax-mcp:** match bestax.io links with an anchored pattern, not a substring ([f210570](f210570)) * **bestax-mcp:** point at a component's page only under a table it can mean ([e45b4b8](e45b4b8)) * **bestax-mcp:** read an overloaded component's implementation, not its first signature ([67f1552](67f1552)) * **bestax-mcp:** resolve links in every string the index ships, and refuse an empty prose title ([e620347](e620347)) * **bestax-mcp:** say that File renders is-empty beside has-name with no name ([590288c](590288c)) * **bestax-mcp:** tag the bestax.io links inside index answers, like the footer's ([d6d4c57](d6d4c57)) * **bestax-mcp:** tell the File lookup to pin an empty name with fileName="" ([188feb0](188feb0)) * **bestax-migrate:** convert a has-name .file with no name only with is-empty ([58a73dd](58a73dd)) * **bestax-migrate:** pin an empty File name so a pick leaves the converted markup as it was ([8bf99d5](8bf99d5)) * **bestax-migrate:** say in the File is-empty TODO that the conversion pins fileName="" ([b837d62](b837d62)) * **bestax-migrate:** say the File is-empty TODO wants the class written, not a condition ([5330608](5330608)) * **bulma-ui:** drop Taginput's fallback name when a label is wired by hand ([2cb9366](2cb9366)) * **bulma-ui:** keep a hasName File's CTA corners rounded while there is no name ([d152cd4](d152cd4)) * **bulma-ui:** keep Input, Select and TextArea labelled through an undefined id ([1658ec1](1658ec1)) * **bulma-ui:** keep the label's id and name through an undefined caller prop ([8fd3457](8fd3457)) * **bulma-ui:** let a caller's aria name win over a group's own label ([c68fb50](c68fb50)) * **bulma-ui:** let a caller's aria-label or aria-labelledby name both range Slider thumbs ([047ff7c](047ff7c)), closes [#974](#974) * **bulma-ui:** name custom form controls from a Field label ([91c9b42](91c9b42)), closes [#939](#939) * **bulma-ui:** name DateRangeInput's default icon in its Control-level warning ([914b92a](914b92a)), closes [#952](#952) * **bulma-ui:** name DateRangeInput's default icon in its Control-level warning ([30c6933](30c6933)), closes [#952](#952) * **bulma-ui:** name each range Slider thumb from its label ([6b962e3](6b962e3)), closes [#981](#981) * **bulma-ui:** stop crediting Bulma's is-empty with a boxed File's round corners ([ebb252b](ebb252b)) * **bulma-ui:** take no Field id for a Taginput at its tag limit ([04b3575](04b3575)), closes [#968](#968) * **create-bestax:** announce the starter's notifications through a status region ([91dbfd5](91dbfd5)) * **create-bestax:** give each starter notification a status region of its own ([f1ef4df](f1ef4df)) * **create-bestax:** keep focus on the counter when Reset disables itself ([733e505](733e505)) * **create-bestax:** keep the reason for the stylesheet order in every scaffolded entry file ([f4591e0](f4591e0)) * **create-bestax:** return focus when the starter's notification closes, and tighten guards ([67e3678](67e3678)) * **create-bestax:** say which helper props render nothing under the no-helpers flavors ([b29985b](b29985b)) * **create-bestax:** stop the no-helpers CLAUDE.md offering helper props as the way out ([a96c203](a96c203)) * **create-bestax:** stop the vite-ts build shadowing its config, and fix the starter page ([1dd964d](1dd964d))
|
🎉 This PR is included in version 4.2.15 🎉 The release is available on: Your semantic-release bot 📦🚀 |
## [1.14.4](https://github.com/allxsmith/bestax/compare/bestax-mcp@1.14.3...bestax-mcp@1.14.4) (2026-10-10) ### Bug Fixes * **bestax-mcp:** release with every bulma-ui release so the published index keeps up ([#1027](#1027)) ([618e483](618e483)) * **bulma-ui:** accept material-symbols 0.47 in the peer range ([539cca2](539cca2)) * **bulma-ui:** drop a Field label's for when nothing it holds takes it ([#1020](#1020)) ([3604c14](3604c14)) * **bulma-ui:** keep a Notification's text clear of its close button ([#1017](#1017)) ([91ebb63](91ebb63)) * **bulma-ui:** keep Theme's component-variable check out of production ([537d90d](537d90d)) * **bulma-ui:** let a caller's aria-label or aria-labelledby name both range Slider thumbs ([047ff7c](047ff7c)), closes [#974](#974) * **bulma-ui:** name a composed DateRangeInputBase's group from a Field label ([75aa6cb](75aa6cb)) * **bulma-ui:** name a range Slider's thumbs from a hand-wired Field label's id ([355065b](355065b)) * **bulma-ui:** name Autocomplete's list from a hand-wired Field label's id ([ad46c06](ad46c06)) * **bulma-ui:** name Autocomplete's suggestion list from a surrounding Field's label ([190183f](190183f)), closes [#998](#998) * **bulma-ui:** name each range Slider thumb from its label ([6b962e3](6b962e3)), closes [#981](#981) * **bulma-ui:** name the date and time picker bases from a Field label ([e7ae797](e7ae797)) * **bulma-ui:** say a label placed in Field.Label reaches no thumbs or list ([07c2a99](07c2a99)) * **bulma-ui:** say FieldLabel renders the label column, not a label ([2a6ab10](2a6ab10)) * **bulma-ui:** say the label prop needs wiring to reach thumbs or list in an inner Field ([9b33d8d](9b33d8d)) * **bulma-ui:** say which controls a hand-wired Field label's id reaches ([514127f](514127f)) * **bulma-ui:** warn when Theme is given a variable Bulma sets on the component ([bb8f9b8](bb8f9b8)), closes [#1021](#1021) * **create-bestax:** announce the starter's notifications through a status region ([91dbfd5](91dbfd5)) * **create-bestax:** give each starter notification a status region of its own ([f1ef4df](f1ef4df)) * **create-bestax:** keep focus on the counter when Reset disables itself ([733e505](733e505)) * **create-bestax:** keep the reason for the stylesheet order in every scaffolded entry file ([f4591e0](f4591e0)) * **create-bestax:** pin material-symbols to the newest range bestax-bulma accepts ([ff4f820](ff4f820)) * **create-bestax:** return focus when the starter's notification closes, and tighten guards ([67e3678](67e3678)) * **create-bestax:** say which helper props render nothing under the no-helpers flavors ([b29985b](b29985b)) * **create-bestax:** stop the no-helpers CLAUDE.md offering helper props as the way out ([a96c203](a96c203)) * **create-bestax:** stop the vite-ts build shadowing its config, and fix the starter page ([1dd964d](1dd964d)) * **docs:** give the mixin note the modifier caveat its lead carries ([1427f08](1427f08)) * **docs:** keep the modifier caveat for mixin variables in the MCP index ([e227663](e227663)) * **docs:** name the one edge of the rule for which variables Theme reaches ([b33ebfa](b33ebfa)) * **docs:** qualify the guide and the theming skill on what Theme reaches ([9c2347b](9c2347b)) * **docs:** read only a component's own mixin as its home ([f40cd2c](f40cd2c)) * **docs:** refuse a CSS-variable scope the page cannot fully word ([c815602](c815602)) * **docs:** say where component-scoped Bulma variables have to be set ([771d5bd](771d5bd)), closes [#1021](#1021)
A labeled
Fieldgave the id its label points at only toInputBase,SelectBaseandTextAreaBase. Every other bestax input drops its ownlabelinside aFieldand took nothing in its place. So inside a labeledField, the Numberinput, Slider, DateInput, TimeInput and DateTimeInput inputs and the Radios and Checkboxes groups had no accessible name. Autocomplete, Taginput, File and Rate were named only by a placeholder or a built-in fallback. axe reported itslabelrule as critical for the single inputs.What changes:
useAutoLabelIdnow adopts the surrounding Field's id when the control renders no label of its own, so each single-input control puts it on its real input. A range Slider gives it to the low thumb, as its own label does. Taginput drops its "Add tag" fallback once a label targets the input.Fieldgives its label an id while the association is on, using the caller'slabelProps.idwhen set.useAutoLabelledBypoints a group'saria-labelledbyat it, so Radios, Checkboxes and Rate are named by the Field's label, and Rate drops its "Rating" fallback.idthe caller sets on the control wins, and the label then names it only throughlabelProps={{ htmlFor }}, as withInputBase. Anaria-labeloraria-labelledbythe caller sets on a group wins, so the Field'saria-labelledbystays off it. A component's ownlabelkeeps its own id wherever it renders, and inside a Field it was already dropped, so the two never compete.labelProps.htmlForon the Field,grouped,hasAddonsand a nested unlabeled Field all still turn the association off.role="group"root througharia-labelledby, as its ownlabeldoes, and each input keeps "Start date" or "End date".undefinedno longer wipes a name: an undefinedidleaves each control its label's id, under a Field's label or its own, and an undefinedaria-labelledbyleaves Rate its fallback.InputBases already do. The docs now say to give each its ownid.The docs follow. The Field accessibility notes and
labelTSDoc describe the wider reach. Each affected control'slabelTSDoc and accessibility notes say a surrounding Field's label names it. The Basic Numberinput and Basic Slider examples now have a label. The Context-Aware Rendering examples use a horizontal Field whose body holds an inner Field, which starts its own scope by design, so those examples now wire the label by hand:labelProps={{ htmlFor }}and anid, orlabelProps={{ id }}andaria-labelledbyfor a group. The Field page's File example set alabelthat the Field drops; it now setsbuttonLabel. The bestax-form skill and a contributor note insrc/form/CLAUDE.mdcover the same ground.Checked in Chromium with Playwright and axe on a page that holds each control inside a labeled Field and inside Field > Control. On main, the single inputs fail axe's
labelrule and most controls have no name. On this branch each control takes its name from the Field label, and thelabelrule passes. File reads the label plus its button text, as it does with its own label.Left out: the horizontal pattern with an inner Field still needs manual wiring for every control, including Input, Select and TextArea, whose pages this leaves alone. The picker
*Basecomponents composed directly in a Field do not adopt the id the wayInputBasedoes.pnpm allpasses locally.Fixes #939
Summary by CodeRabbit
New Features
Fieldnow automatically provide accessible names for supported single controls and control groups, with caller-provided labels and ARIA naming taking precedence.Bug Fixes
Documentation