Skip to content

Commit 76d72eb

Browse files
committed
docs: composition notes
1 parent 48243a4 commit 76d72eb

File tree

1 file changed

+68
-0
lines changed

1 file changed

+68
-0
lines changed

website/data/guides/composition.mdx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,74 @@ function Example() {
156156
In the example above, you will notice that the popover and tooltip trigger share
157157
the 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

161229
Internally, we use DOM query methods like `document.querySelectorAll` and

0 commit comments

Comments
 (0)