Migrating from shadcn/ui
shadcn/ui isn’t a dependency you uninstall — it’s Radix UI + Tailwind code that lives in your repo (components/ui/*). So this isn’t a rip-and-replace migration. It’s co-existence: Wire UI sits next to shadcn, reuses the same design tokens, and fills the gaps shadcn doesn’t cover.
Two facts make this easy:
data-state model, same Tailwind styling approach as Wire UI. If you ever swap one out, the Radix map applies directly.—primary, —border, —ring, …) you already have — the two look identical.Framework note. shadcn/ui is React, but the same approach holds for shadcn-vue and shadcn-solid (both Reka-UI/Kobalte based). Wire UI styles identically in all three — toggle the picker on the examples below. The only syntax difference is
className(React/Solid) vsclass(Vue) and how children/props are passed.
Why add Wire UI to a shadcn project
Reach for Wire UI for the primitives shadcn/Radix never shipped — without leaving your design system:
- AI-native primitives —
Chat,Markdown,CodeBlock,Mention,Stream,Citation. - Richer inputs —
Calendar,Combobox,Command,ColorPicker,OTP,FileUpload,TagInput. - Multi-framework — the same API in Vue and Solid, which shadcn (React-only) can’t offer.
The shadcn-compat theme
Because shadcn styling is just Tailwind classes over CSS variables, you style a Wire UI primitive with the same classes and it matches pixel-for-pixel. Here’s a Wire UI Button wearing shadcn’s default button styling:
import { Button } from '@wire-ui/react'
export function ShadcnButton(props: React.ComponentProps<typeof Button>) {
return (
<Button
className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md
text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1
focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50
bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2"
{...props}
/>
)
}For stateful components, map shadcn’s Radix data-[state=…] selectors to Wire UI’s attributes. A shadcn Switch keys off data-[state=checked]; Wire UI’s Switch exposes data-checked:
import { Switch } from '@wire-ui/react'
<Switch.Root className="peer inline-flex h-5 w-9 shrink-0 items-center rounded-full border-2
border-transparent transition-colors bg-input data-[checked]:bg-primary">
<Switch.Thumb className="pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg
transition-transform translate-x-0 data-[checked]:translate-x-4" />
</Switch.Root>The only real translation is the attribute name — see Data Attributes for the full Wire UI set (e.g. data-state on Accordion/Tabs, data-checked on Switch/Checkbox, data-highlighted on menu items).
A drop-in pattern
To match shadcn’s variant/size API so your team’s muscle memory survives, wrap a Wire UI primitive with cva — the same tool shadcn already uses:
import { cva, type VariantProps } from 'class-variance-authority'
import { Button as WireButton } from '@wire-ui/react'
import { cn } from '@/lib/utils'
const buttonVariants = cva('inline-flex items-center justify-center rounded-md text-sm font-medium …', {
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
outline: 'border border-input bg-background hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
},
size: { default: 'h-9 px-4 py-2', sm: 'h-8 px-3', lg: 'h-10 px-6' },
},
defaultVariants: { variant: 'default', size: 'default' },
})
export function Button({ className, variant, size, ...props }:
React.ComponentProps<typeof WireButton> & VariantProps<typeof buttonVariants>) {
return <WireButton className={cn(buttonVariants({ variant, size }), className)} {...props} />
}Drop that in components/ui/button.tsx and it’s indistinguishable from a shadcn button — backed by a Wire UI primitive that also runs in Vue and Solid. cva is framework-agnostic: in Vue bind buttonVariants(...) to :class, in Solid to class — the variant logic is identical.
Strategy
cva wrapper.globals.css variables, so a theme change updates shadcn and Wire UI together.