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 ships as codemods. Run list to see every transform, then run the ones you need against your files.

# 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.

Beyond as-child-to-render, prop renames ship for every framework — react/*, solid/*, svelte/*, and vue/* (Vue rewrites kebab and bound props too):

TransformWhat it does
carousel-propsslideCountcount, autoplayautoPlay, paddingitemSpacing
floating-panel-placementresizeTriggerAxesresizeTriggerPlacements, axisplacement
image-cropper-placementhandlesplacements, positionplacement
tabs-virtual-focuscompositevirtualFocus (value inverted)
popover-portalledremoves the portalled prop
tags-input-editableadds editable to keep the old default
pin-input-countlengthcount, or flags a missing count

Stylesheets have their own transform, css/data-attributes, covered under Data attributes are merged below.

Two changes are left to do by hand because they reshape markup, not just props: the popover api.portalled ? Portal : Fragment wrapper (drop it for a plain Portal), and the Combobox/Listbox/Select contentcontent + list split described under the list part.

Migrate with an AI agent

The codemod handles asChildrender. 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, props is a merge function: call props({ 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.

Popover is no longer portalled by prop

The portalled prop and api.portalled value are gone. The popover now detects whether its content is portalled from where you render it and proxies tab order accordingly, so a forgotten portalled can no longer break keyboard access. Decide portalling by rendering the content inside Portal or not.

- <Popover.Root portalled>
+ <Popover.Root>
    <Popover.Trigger>Open</Popover.Trigger>
+   <Portal>
      <Popover.Positioner>
        <Popover.Content>...</Popover.Content>
      </Popover.Positioner>
+   </Portal>
  </Popover.Root>

The react/popover-portalled codemod (and its solid/svelte/vue variants) removes the prop; wrap the content in Portal yourself, as this is markup the codemod won't reshape. To render inline, drop the Portal — tab order is still handled.

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).

Combobox/Listbox/Select gain a List part

The listbox semantics (role="listbox", active-descendant, keyboard focus) move off Content onto a new List part. Content becomes a plain wrapper, so you can render headers, footers, or a search input in the popup without polluting the listbox. Wrap the items in List inside Content. This is structural, so the codemod leaves it to you.

  <Combobox.Content>
+   <Combobox.List>
      {items.map((item) => (
        <Combobox.Item key={item.value} item={item}>{item.label}</Combobox.Item>
      ))}
+   </Combobox.List>
  </Combobox.Content>

The composite prop is gone; pass popupType="dialog" where you previously set composite={false}.

Data attributes are merged

data-scope, data-part, and data-ownedby collapse into one attribute per part, data-{scope}-{part}. An element can now take part in more than one machine, and selectors get shorter. This does not change your JSX — it changes the CSS and any querySelector that targets these attributes.

- [data-scope="dialog"][data-part="trigger"] { ... }
+ [data-dialog-trigger] { ... }

The css/data-attributes transform rewrites stylesheets: it merges scope/part selectors, turns toggle [data-state="on"] into [data-pressed], and swaps the removed data-focus on toggle-group and toolbar for :focus-within / :focus-visible.

npx @ark-ui/codemod css/data-attributes "src/**/*.css" --dry

Attributes referenced from JavaScript strings, or selectors where scope and part are not adjacent, are left for you to update.

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). StepsStepChangeDetailsStepChangeDetails, ColorPickerColorColor, TabsContentState / TabsTriggerStateTabContentState / TabTriggerState. Internals such as CheckboxProvider and splitCollapsibleProps are no longer exported.