Skip to Content
⭐️ Leave a star →
From Headless UI

Migrating from Headless UI

Headless UI and Wire UI are close cousins — both are unstyled, accessible primitives you bring your own styling to. The migration is mostly mechanical, with two real differences to internalize:

Render props → data-* state. Where Headless UI gives you {({ open }) => …}, Wire UI exposes data-state / data-open attributes you style with plain CSS — no render functions.
asasChild. Headless UI’s as prop for polymorphism becomes Wire UI’s asChild.
Different part names. Menu.ItemsDropdown.Menu, Dialog.PanelModal.Content, etc. — so expect more hand-tuning than a Radix migration.
More components. Wire UI adds everything Headless UI never shipped — Calendar, Tooltip, Toast, the AI primitives, and more.

Headless UI ships for React and Vue. Wire UI covers both (plus Solid). The codemod below is for React; Vue is a manual import swap of the same map.

1. Swap the dependency

npm uninstall @headlessui/react # or @headlessui/vue npm install @wire-ui/react # or @wire-ui/vue / @wire-ui/solid

2. Run the codemod (React)

npx jscodeshift \ -t https://wire-ui.com/codemods/headless-ui-to-wire-ui.cjs \ --extensions=tsx,ts,jsx,js --parser=tsx \ src/

It repoints @headlessui/react imports to @wire-ui/react, renames the top-level components (MenuDropdown, DialogModal, ListboxSelect, TabTabs), and drops Transition (no equivalent). Because Headless UI’s part names differ, the renamed parts (Dropdown.Items, Modal.Panel) will then show as type errors — that’s your fix-it checklist.

Component map

✅ drop-in · ✏️ codemod renames it · 🔧 rename + adjust parts

Headless UIWire UINotes
SwitchSwitch🔧Now compound: Switch.Root + Switch.Thumb. checked/onChange carry over.
PopoverPopover✏️Popover.Button/PanelPopover.Trigger/Content.
ComboboxCombobox🔧Combobox.Input/Options/OptionCombobox.Input/Content/Item.
MenuDropdown🔧Menu.Button/Items/ItemDropdown.Trigger + Dropdown.Menu (items are plain children).
ListboxSelect🔧Listbox.Button/Options/OptionSelect.Trigger/Content/Item.
DialogModal🔧Dialog.PanelModal.Content nested inside Modal.Overlay; Dialog.Title → a plain heading.
DisclosureAccordion🔧One-item Accordion type="single" collapsible; Disclosure.Button/PanelAccordion.Trigger/Content.
RadioGroupRadio🔧RadioGroup.OptionRadio.Item (+ Radio.Indicator/Label).
Tab groupTabs🔧Tab.Group/List/Tab/Panels/PanelTabs.Root/List/Trigger/Content.
Field/Label/DescriptionForm.*✏️See the Form Integration guide.
Button/Input/Textarea/CheckboxsameThe v2 form controls map by name.
Transition🔧No Transition primitive. Drive enter/exit from data-state with CSS or your animation library.

Worked example: Menu → Dropdown

Note how the render-prop + part structure flattens into compound parts with data-* state.

Before (Headless UI):

import { Menu } from '@headlessui/react' <Menu> <Menu.Button>Options</Menu.Button> <Menu.Items> <Menu.Item>{({ active }) => <a data-active={active}>Edit</a>}</Menu.Item> <Menu.Item><a>Delete</a></Menu.Item> </Menu.Items> </Menu>

After (Wire UI):

import { Dropdown } from '@wire-ui/react' <Dropdown.Root> <Dropdown.Trigger>Options</Dropdown.Trigger> <Dropdown.Menu> {/* style the active item with [data-highlighted] instead of a render prop */} <button>Edit</button> <button>Delete</button> </Dropdown.Menu> </Dropdown.Root>

After the codemod — checklist

Let tsc run — every renamed part (Dropdown.Items, Modal.Panel, …) surfaces as the exact line to fix against the map.
Replace render-prop children ({({ open, active }) => …}) with [data-state] / [data-highlighted] CSS — see Data Attributes.
Swap Transition for a data-state-driven CSS transition or your animation library of choice.
Rework DialogModal structure (Content inside Overlay, your own trigger button) — see the overlay walkthrough.
Last updated on

MIT License © 2026 wire-ui

Migrating from Headless UI – Wire UI