Bug report
Checkbox and Switch machines unconditionally set aria-labelledby on the hidden input via getHiddenInputProps(), even when no label element exists in the DOM. This creates a broken ARIA reference that points to a non-existent element.
Steps to reproduce
- Render a Checkbox (or Switch) inside a Field without a
Field.Label
- Provide an accessible name via
aria-label instead of a visible label
- Inspect the hidden input element
- The input has
aria-labelledby="field::v-0::label" pointing to an element that doesn't exist
Link to reproduction
Stackblitz reproduction: https://stackblitz.com/edit/lpxfxp4j?file=src%2FApp.vue&showSidebar=0
Expected behaviour
When no label element is rendered, getHiddenInputProps() should not set aria-labelledby. The attribute should only be present when a corresponding label element actually exists in the DOM.
When aria-label is the only labelling mechanism, the hidden input should have aria-label without a conflicting aria-labelledby pointing to nothing.
Possible Solution
In checkbox.connect.ts and switch.connect.ts, getHiddenInputProps() unconditionally sets:
"aria-labelledby": getLabelId(scope),
getLabelId always returns a string (it falls back to a generated ID), so aria-labelledby is always set.
One fix would be to only include aria-labelledby when a label element is present in the DOM:
"aria-labelledby": scope.getById(getLabelId(scope)) ? getLabelId(scope) : undefined,
Or alternatively, skip it when the consumer has provided aria-label directly.
The same pattern exists in both @zag-js/checkbox and @zag-js/switch. Radio is not affected because RadioGroup.Item manages labelling independently.
System information
| Software |
Version(s) |
| Zag Version |
1.31.1 |
| Browser |
All |
| Operating System |
All |
Your Company/Team
Deputy / Design System team
Additional information
The issue is amplified by the Ark UI Field integration. In @ark-ui/vue, useCheckbox overrides the machine's ids.label with field.ids.label:
ids: {
label: field?.value.ids.label,
hiddenInput: field?.value.ids.control
},
So even if the checkbox's own Checkbox.Label is rendered, the aria-labelledby points to the Field's label ID instead of the checkbox label's ID. When no Field.Label is rendered, this creates the broken reference.
Bug report
Checkbox and Switch machines unconditionally set
aria-labelledbyon the hidden input viagetHiddenInputProps(), even when no label element exists in the DOM. This creates a broken ARIA reference that points to a non-existent element.Steps to reproduce
Field.Labelaria-labelinstead of a visible labelaria-labelledby="field::v-0::label"pointing to an element that doesn't existLink to reproduction
Stackblitz reproduction: https://stackblitz.com/edit/lpxfxp4j?file=src%2FApp.vue&showSidebar=0
Expected behaviour
When no label element is rendered,
getHiddenInputProps()should not setaria-labelledby. The attribute should only be present when a corresponding label element actually exists in the DOM.When
aria-labelis the only labelling mechanism, the hidden input should havearia-labelwithout a conflictingaria-labelledbypointing to nothing.Possible Solution
In
checkbox.connect.tsandswitch.connect.ts,getHiddenInputProps()unconditionally sets:getLabelIdalways returns a string (it falls back to a generated ID), soaria-labelledbyis always set.One fix would be to only include
aria-labelledbywhen a label element is present in the DOM:Or alternatively, skip it when the consumer has provided
aria-labeldirectly.The same pattern exists in both
@zag-js/checkboxand@zag-js/switch. Radio is not affected becauseRadioGroup.Itemmanages labelling independently.System information
Your Company/Team
Deputy / Design System team
Additional information
The issue is amplified by the Ark UI Field integration. In
@ark-ui/vue,useCheckboxoverrides the machine'sids.labelwithfield.ids.label:So even if the checkbox's own
Checkbox.Labelis rendered, thearia-labelledbypoints to the Field's label ID instead of the checkbox label's ID. When noField.Labelis rendered, this creates the broken reference.