@@ -156,6 +156,74 @@ function Example() {
156156In the example above, you will notice that the popover and tooltip trigger share
157157the same id. That's how to compose machines together.
158158
159+ ### Important: Customizing IDs and ARIA attributes
160+
161+ #### Always use the ` ids ` context option
162+
163+ When customizing element IDs, always use the ` ids ` option in the machine context.
164+ Never manually set the ` id ` attribute on elements using the prop functions.
165+
166+ ``` tsx
167+ // ❌ Wrong: Manually setting id on the element
168+ const api = checkbox .connect (service , normalizeProps )
169+ return <label { ... api .getLabelProps ()} id = " my-custom-id" >Label</label >
170+
171+ // ✓ Correct: Use the ids option in machine context
172+ const service = useMachine (checkbox .machine , {
173+ ids: { label: " my-custom-id" },
174+ })
175+ const api = checkbox .connect (service , normalizeProps )
176+ return <label { ... api .getLabelProps ()} >Label</label >
177+ ```
178+
179+ ** Why?** Zag needs to know about custom IDs to properly generate ARIA reference
180+ attributes across all related elements. When you set IDs manually, the machine
181+ can't update the corresponding ` aria-labelledby ` , ` aria-describedby ` , and other
182+ reference attributes.
183+
184+ #### Pitfall: Custom IDs with missing elements
185+
186+ When you configure custom IDs via the ` ids ` option, Zag generates ARIA reference
187+ attributes (like ` aria-labelledby ` ) based on those IDs, regardless of whether
188+ the elements exist in the DOM. This can break accessibility if you configure IDs
189+ for elements you don't render.
190+
191+ ``` tsx
192+ // ❌ Wrong: Configuring label ID but not rendering the label
193+ const service = useMachine (checkbox .machine , {
194+ ids: { label: " custom-label-id" },
195+ })
196+ return (
197+ <div { ... api .getControlProps ()} >
198+ { /* Control has aria-labelledby="custom-label-id" but label doesn't exist */ }
199+ <input { ... api .getHiddenInputProps ()} />
200+ </div >
201+ )
202+
203+ // ✓ Correct: Only configure IDs for elements you render
204+ const service = useMachine (checkbox .machine , {
205+ ids: { control: " custom-control-id" },
206+ // Don't set label ID if you're not rendering it
207+ })
208+ return (
209+ <div { ... api .getControlProps ()} aria-label = " Checkbox" >
210+ <input { ... api .getHiddenInputProps ()} />
211+ </div >
212+ )
213+
214+ // ✓ Better: Keep the label but hide it visually
215+ return (
216+ <div >
217+ <label { ... api .getLabelProps ()} className = " visually-hidden" >
218+ Checkbox
219+ </label >
220+ <div { ... api .getControlProps ()} >
221+ <input { ... api .getHiddenInputProps ()} />
222+ </div >
223+ </div >
224+ )
225+ ```
226+
159227## Custom window environment
160228
161229Internally, we use DOM query methods like ` document.querySelectorAll ` and
0 commit comments