Upgrade Guide
What changed in Ark UI v6, and the breaking changes to fix as you migrate.
v6 keeps the components you already know and unifies how you compose them. Polymorphism moves to a
single render function, every part forwards its machine state, and the frameworks line up so the
same anatomy works across React, Solid, Svelte, and Vue. This guide covers the breaking changes and
how to migrate.
Automated migration
Most of the mechanical work — chiefly the asChild → render rename — ships as a codemod.
# See every available transform
npx @ark-ui/codemod list
# Preview the changes without writing them
npx @ark-ui/codemod react/as-child-to-render "src/**/*.tsx" --dry
--dry prints a diff and writes nothing. Without it the codemod refuses to run on a dirty working
tree, so a bad run is one git checkout away. Anything ambiguous is left untouched and reported with
a file and a reason rather than guessed.
Only Ark UI elements are rewritten — an asChild from another library in the same file is left
alone. Parts reached through local barrels, re-exports, or factory wrappers (styled(ark.button))
are resolved with the --cross-file flag.
Migrate with an AI agent
The codemod handles asChild → render. For the rest, paste this prompt into an AI agent (Claude
Code, Cursor, Copilot) pointed at your codebase — it has the full context for the manual changes.
You are migrating this codebase from Ark UI v5 to v6. Work framework-aware
(React, Solid, Svelte, or Vue — detect which one this project uses) and change
only Ark UI usage. After each step, run the project's typecheck/build.
1. Run the codemod for the mechanical rename, then review its report:
npx @ark-ui/codemod react/as-child-to-render "src/**/*.tsx"
(swap `react` for solid/svelte/vue; add `--cross-file` if parts are reached
through local barrels, re-exports, or factory wrappers like styled(ark.button).)
2. Finish anything the codemod left. `asChild` is now a `render` function that
receives (props, state):
- React/Vue: move the child into the function/slot; spread `props`.
- Solid/Svelte/Vue: `props` is a MERGE FUNCTION — spread `{...props()}`
(Solid/Svelte) or `v-bind="props()"` (Vue), and call `props({ onClick })`
to merge your handlers instead of overwriting them. In Solid, `state` is an
accessor: `state().open`.
3. Toaster is no longer polymorphic: remove any `render`/`asChild` on it and
drop `ToasterState`. Style the group with CSS (`data-placement`, `data-side`,
`data-align`) or wrap your own element around it.
4. Combobox.Empty / Listbox.Empty move OUT of List to become a sibling of List.
Put the styled message in a child element. For loading/error states, use the
new `Status` part on Combobox, Listbox, and Select.
5. useCollator / useFilter now return values directly (no accessor/.value) on
Solid, Svelte, and Vue: `collator.compare(a, b)`, `const { contains } = useFilter(...)`.
6. Svelte only:
- The `useX` hooks now require an `id`: `useCollapsible({ id })` where
`const id = $props.id()`. `$props.id()` may be called once — derive if you
need two. Components like `<Collapsible.Root>` are unaffected.
- Exports now match the other frameworks: Dialog parts are prefixed
(`DialogPositioner`, `DialogRoot`, ...); `StepsStepChangeDetails` →
`StepChangeDetails`, `ColorPickerColor` → `Color`, `TabsContentState` /
`TabsTriggerState` → `TabContentState` / `TabTriggerState`. Internals like
`CheckboxProvider` and `splitCollapsibleProps` are no longer exported.
Report anything ambiguous instead of guessing. Full guide:
https://ark-ui.com/docs/overview/upgrade-guide
Breaking changes to fix
These are the changes you have to act on when you move a project from v5.
asChild is now render
Polymorphism moves from a wrapper prop to a render function that receives the part's props and its
state. Every part gets one typed composition API, and you can read machine state inline.
The shape differs per framework. React and Vue move the child into the function or slot; Solid and Svelte are a rename where the props object becomes a props function you must call.
// React
- <Collapsible.Trigger asChild>
- <button>Toggle</button>
- </Collapsible.Trigger>
+ <Collapsible.Trigger render={(props, state) => (
+ <button {...props}>{state.open ? 'Open' : 'Closed'}</button>
+ )} />
// Solid — {...props} becomes {...props()}, state is an accessor
- <Collapsible.Trigger asChild>
- {(props) => <button {...props}>Toggle</button>}
- </Collapsible.Trigger>
+ <Collapsible.Trigger render={(props, state) => (
+ <button {...props()}>{state().open ? 'Open' : 'Closed'}</button>
+ )} />
<!-- Svelte -->
<Collapsible.Trigger>
{#snippet render(props, state)}
<button {...props()}>{state.open ? 'Open' : 'Closed'}</button>
{/snippet}
</Collapsible.Trigger>
<!-- Vue -->
<Collapsible.Trigger>
<template #render="{ props, state }">
<button v-bind="props()">{{ state.open ? 'Open' : 'Closed' }}</button>
</template>
</Collapsible.Trigger>
In Solid, Svelte, and Vue,
propsis a merge function: callprops({ onClick: mine })to merge your handlers with the part's instead of overwriting them.
Toaster is no longer polymorphic
Toaster no longer accepts render or asChild, and ToasterState is gone. Its children belong to
the machine, so handing them over dropped toasts. Style the group with CSS — it carries
data-placement, data-side, and data-align — or wrap your own element around Toaster.
Combobox.Empty and Listbox.Empty move out of List
Empty used to render inside List and unmount when the collection was non-empty, which kept it out
of the accessibility tree and prevented any announcement. It now stays mounted as a sibling of List
with role="status", swapping only its children. Move it out of List and put your styled message in
a child element.
<Combobox.Content>
- <Combobox.List>
- <Combobox.Empty className={styles.Item}>No results found</Combobox.Empty>
+ <Combobox.Empty>
+ <div className={styles.Empty}>No results found</div>
+ </Combobox.Empty>
+ <Combobox.List>
{/* items */}
</Combobox.List>
</Combobox.Content>
A new Status part on Combobox, Listbox, and Select covers loading and error states that
Empty cannot distinguish (it keys off collection.size === 0).
useCollator and useFilter return values
For Solid, Svelte, and Vue, these hooks now return their values directly instead of an accessor, matching React. They still follow locale changes.
- const collator = useCollator()
- collator().compare(a, b) // Solid, Svelte
- collator.value.compare(a, b) // Vue
+ const collator = useCollator()
+ collator.compare(a, b)
- const filters = useFilter({ sensitivity: 'base' })
- filters().contains(text, query) // Solid, Svelte
- filters.value.contains(text, query) // Vue
+ const { contains } = useFilter({ sensitivity: 'base' })
+ contains(text, query)
Svelte: hooks require an id
After moving to Zag 2, the useX hooks require an id. Without one, two instances on a page
generated the same element ids. Components (<Collapsible.Root>) are unaffected and still mint their
own.
- const collapsible = useCollapsible()
+ const id = $props.id()
+ const collapsible = useCollapsible({ id })
$props.id() may only be called once per component; derive from it if you need two.
Svelte: export parity
Svelte exports now match the other frameworks. Dialog's Positioner, Root, RootProvider,
Title, and Trigger are prefixed (DialogPositioner). StepsStepChangeDetails →
StepChangeDetails, ColorPickerColor → Color, TabsContentState / TabsTriggerState →
TabContentState / TabTriggerState. Internals such as CheckboxProvider and splitCollapsibleProps
are no longer exported.