{"slug":"composition","title":"Composition","description":"Best practices to compose events and components","contentType":"guides","content":"## Event composition\n\nZag encourages the use of spread props to ensure we automatically attach all the\nevent handlers and properties to elements.\n\nSometimes, you may want to attach your own event handlers to the elements. To do\nthat, import the `mergeProps` utility provided by zag for your framework.\n\n```jsx\n// 1. import mergeProps utility\nimport { useMachine, mergeProps } from \"@zag-js/react\"\nimport * as hoverCard from \"@zag-js/hover-card\"\n\nexport function Toggle() {\n  const service = useMachine(hoverCard.machine, {\n    id: \"1\",\n  })\n  const api = hoverCard.connect(service)\n\n  // 2. write your custom event handlers\n  const handleHover = () => {\n    // do something\n  }\n\n  // 3. merge the props\n  const triggerProps = mergeProps(api.getTriggerProps(), {\n    onPointerEnter: handleHover,\n  })\n\n  return (\n    // 4. spread the new props\n    <a href=\"https://twitter.com/zag_js\" target=\"_blank\" {...triggerProps}>\n      {api.open ? \"Open\" : \"Close\"}\n    </a>\n  )\n}\n```\n\n## Id composition\n\nZag depends heavily on pure DOM queries to identify elements. This means every\nelement part needs to have a unique id.\n\nEach time you initiate the machine with the `useMachine` hook, you'll need to\nensure that you provide a unique id.\n\nIn most cases, you can rely on the framework providing a unique id for each\nmachine.\n\n### React\n\n```jsx\nimport * as accordion from \"@zag-js/accordion\"\nimport { useMachine, normalizeProps } from \"@zag-js/framework\"\nimport { useId } from \"react\"\n\nfunction Component() {\n  const service = useMachine(accordion.machine, { id: useId() })\n  const api = machine.connect(service, normalizeProps)\n\n  // ...\n}\n```\n\nSee [useId](https://react.dev/reference/react/useId).\n\n### Solid\n\n```jsx\nimport * as accordion from \"@zag-js/accordion\"\nimport { useMachine, normalizeProps } from \"@zag-js/solid\"\nimport { createUniqueId } from \"solid-js\"\n\nfunction Component() {\n  const service = useMachine(accordion.machine, { id: createUniqueId() })\n  const api = machine.connect(service, normalizeProps)\n\n  // ...\n}\n```\n\nSee\n[createUniqueId](https://docs.solidjs.com/reference/component-apis/create-unique-id).\n\n### Vue\n\n```html\n<script setup>\n  import * as accordion from \"@zag-js/accordion\"\n  import { useMachine, normalizeProps } from \"@zag-js/vue\"\n  import { useId } from \"vue\"\n\n  const service = useMachine(accordion.machine, { id: useId() })\n  const api = machine.connect(service, normalizeProps)\n</script>\n\n<template>\n  <!-- ... -->\n</template>\n```\n\nSee [useId](https://vuejs.org/api/composition-api-helpers#useid).\n\n### Svelte\n\n```html\n<script>\n  import * as accordion from \"@zag-js/accordion\"\n  import { useMachine, normalizeProps } from \"@zag-js/svelte\"\n\n  const id = $props.id()\n  const service = useMachine(accordion.machine, { id })\n  const api = machine.connect(service, normalizeProps)\n</script>\n\n<!-- ... -->\n```\n\nSee [$props.id](<https://svelte.dev/docs/svelte/$props#$props.id()>).\n\nInternally, Zag maps the unique id provided to each component parts needed for\nthe widget to work.\n\nIn some cases, you might want to compose different machines together in a single\ncomponent. For example, you want to use the same trigger as a popover and\ntooltip trigger at the same time.\n\nTo achieve this, you will need to pass custom `ids` to the machine's context.\nThis will ensure that calling `document.getElementById(...)` within the tooltip\nand/or popover will return the same element.\n\n```tsx {6,10}\nimport * as tooltip from \"@zag-js/tooltip\"\nimport * as popover from \"@zag-js/popover\"\n\nfunction Example() {\n  const tooltipService = useMachine(tooltip.machine, {\n    ids: { trigger: \"id-1\" },\n  })\n\n  const popoverService = useMachine(popover.machine, {\n    ids: { trigger: \"id-1\" },\n  })\n\n  // ...\n}\n```\n\nIn the example above, you will notice that the popover and tooltip trigger share\nthe same id. That's how to compose machines together.\n\n### Important: Customizing IDs and ARIA attributes\n\n#### Always use the `ids` context option\n\nWhen customizing element IDs, always use the `ids` option in the machine context.\nNever manually set the `id` attribute on elements using the prop functions.\n\n```tsx\n// ❌ Wrong: Manually setting id on the element\nconst api = checkbox.connect(service, normalizeProps)\nreturn <label {...api.getLabelProps()} id=\"my-custom-id\">Label</label>\n\n// ✓ Correct: Use the ids option in machine context\nconst service = useMachine(checkbox.machine, {\n  ids: { label: \"my-custom-id\" },\n})\nconst api = checkbox.connect(service, normalizeProps)\nreturn <label {...api.getLabelProps()}>Label</label>\n```\n\n**Why?** Zag needs to know about custom IDs to properly generate ARIA reference\nattributes across all related elements. When you set IDs manually, the machine\ncan't update the corresponding `aria-labelledby`, `aria-describedby`, and other\nreference attributes.\n\n#### Pitfall: Custom IDs with missing elements\n\nWhen you configure custom IDs via the `ids` option, Zag generates ARIA reference\nattributes (like `aria-labelledby`) based on those IDs, regardless of whether\nthe elements exist in the DOM. This can break accessibility if you configure IDs\nfor elements you don't render.\n\n```tsx\n// ❌ Wrong: Configuring label ID but not rendering the label\nconst service = useMachine(checkbox.machine, {\n  ids: { label: \"custom-label-id\" },\n})\nreturn (\n  <div {...api.getControlProps()}>\n    {/* Control has aria-labelledby=\"custom-label-id\" but label doesn't exist */}\n    <input {...api.getHiddenInputProps()} />\n  </div>\n)\n\n// ✓ Correct: Only configure IDs for elements you render\nconst service = useMachine(checkbox.machine, {\n  ids: { control: \"custom-control-id\" },\n  // Don't set label ID if you're not rendering it\n})\nreturn (\n  <div {...api.getControlProps()} aria-label=\"Checkbox\">\n    <input {...api.getHiddenInputProps()} />\n  </div>\n)\n\n// ✓ Better: Keep the label but hide it visually\nreturn (\n  <div>\n    <label {...api.getLabelProps()} className=\"visually-hidden\">\n      Checkbox\n    </label>\n    <div {...api.getControlProps()}>\n      <input {...api.getHiddenInputProps()} />\n    </div>\n  </div>\n)\n```\n\n## Custom window environment\n\nInternally, we use DOM query methods like `document.querySelectorAll` and\n`document.getElementById` to locate elements within the machine.\n\nIn custom environments like iframe, Shadow DOM, Electron, etc., the machine\nmight not work as expected because `document` may not be available.\n\nTo provide the correct reference to root node or document, you can pass\n`getRootNode` function it to the machine's context.\n\n> In shadow DOM, the root node can be derived from calling\n> `element.getRootNode()` method on any element.\n\n```jsx {12,16,42}\nimport * as accordion from \"@zag-js/accordion\"\nimport { useMachine, normalizeProps } from \"@zag-js/react\"\nimport Frame, { useFrame } from \"react-frame-component\"\n\nconst data = [\n  { title: \"Watercraft\", content: \"Sample accordion content\" },\n  { title: \"Automobiles\", content: \"Sample accordion content\" },\n  { title: \"Aircraft\", content: \"Sample accordion content\" },\n]\n\nfunction Accordion({ id }) {\n  const { document } = useFrame()\n\n  const service = useMachine(accordion.machine, {\n    id,\n    getRootNode: () => document,\n  })\n\n  const api = accordion.connect(service, normalizeProps)\n\n  return (\n    <div {...api.getRootProps()}>\n      {data.map((item, index) => (\n        <div key={index} {...api.getItemProps({ value: item.title })}>\n          <h3>\n            <button {...api.getTriggerProps({ value: item.title })}>\n              {item.title}\n            </button>\n          </h3>\n          <div {...api.getContentProps({ value: item.title })}>\n            {item.content}\n          </div>\n        </div>\n      ))}\n    </div>\n  )\n}\n\nexport default function App() {\n  return (\n    <div className=\"App\">\n      <h1>ZagJs in Iframe</h1>\n      <Frame height=\"200px\" width=\"100%\">\n        <Accordion id=\"2\" />\n      </Frame>\n    </div>\n  )\n}\n```","editUrl":"https://github.com/chakra-ui/zag/edit/main/website/data/guides/composition.mdx"}