Every React project eventually reaches for shadcn/ui. Every Vue project building the same kind of dashboard now reaches for its little sibling, shadcn-vue. Both promise the same pitch: no npm-installed component library locking you into someone else’s design decisions, just copy-pasted, fully-owned code sitting in your own repo. But the setup paths, the primitives underneath, and the day-to-day developer experience diverge in ways that matter the moment you start shipping real screens. This tutorial walks through building the identical settings page twice — once with React 19.2 and shadcn/ui’s CLI v4, once with Vue 3.5 and shadcn-vue 2.4.3 — so you can see exactly where the two toolchains agree, where they split, and which one fits your stack.
By the end you will have two working component libraries, a themed button and dialog, a data table, and a dark-mode toggle, built entirely from code you own rather than a package you depend on. Total time: roughly 90 minutes if you follow along in both frameworks, 45 if you only need one.
What shadcn/ui and shadcn-vue actually are
Neither shadcn/ui nor shadcn-vue is a traditional npm dependency in the way Material UI or Vuetify are. Both are described by their maintainers as “code distribution platforms” rather than component libraries. Instead of installing a package and importing a `
On the React side, shadcn/ui is built on Radix UI primitives for accessibility and behavior, styled with Tailwind CSS. Components ship as TypeScript `.tsx` files using React hooks and JSX. On the Vue side, shadcn-vue was originally built on Radix Vue, but the project’s changelog confirms the default registry has since moved to Reka UI — described by its own maintainers as “previously Radix Vue” — while a `shadcn-vue@radix` install path still exists for teams that want to stay on the older primitive library. Components in shadcn-vue are Vue single-file components using the Composition API and Reka UI’s Vue-native primitive API.
That single difference — Radix UI (React-only) versus Reka UI (Vue-native) — is the real architectural fork between the two projects. It is not a thin styling skin over the same engine; it is two separately maintained accessibility-primitive libraries that happen to produce visually identical output.
Prerequisites and versions
Confirm these versions before starting. Version drift is the single biggest cause of CLI init failures in both ecosystems.
| Tool | Version used in this tutorial | Minimum required |
|---|---|---|
| Node.js | 22 LTS | 18.17+ |
| React | 19.2 | 18+ (19 required for latest CLI templates) |
| Vue | 3.5.x | 3.3+ |
| Vite | 8.x | 5+ |
| Nuxt (optional, Vue meta-framework) | 4.4.x | 3.9+ |
| Tailwind CSS | 4.3.x | 3.4+ (v4 recommended) |
| shadcn CLI (React) | v4 (package name `shadcn`) | — |
| shadcn-vue CLI | 2.4.3 | — |
| Package manager | pnpm 9 or npm 10 | npm 9+ |
You will also need a code editor with TypeScript support, a terminal, and roughly 500MB of free disk space per project for `node_modules`. Nothing here requires a paid account — both toolchains are fully open source.
Step 1: Scaffold the React project
Start with a fresh Vite-powered React app. shadcn/ui works with Next.js too, but Vite keeps this comparison framework-neutral on the React side.
npm create vite@latest react-shadcn-demo -- --template react-ts
cd react-shadcn-demo
npm install
Install Tailwind CSS 4 next. Tailwind v4 dropped the old `tailwind.config.js`-heavy setup in favor of a CSS-first configuration, which the shadcn CLI now detects automatically.
npm install tailwindcss @tailwindcss/vite
npm install -D @types/node
Add the Tailwind Vite plugin to `vite.config.ts` and set up path aliases, since shadcn’s generated imports rely on the `@/` alias pointing at `src/`.
import path from "path"
import tailwindcss from "@tailwindcss/vite"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
Step 2: Scaffold the Vue project
Now build the mirror project in Vue. Nuxt 4 is the more common real-world target for shadcn-vue, but plain Vite + Vue keeps the comparison even.
npm create vite@latest vue-shadcn-demo -- --template vue-ts
cd vue-shadcn-demo
npm install
npm install tailwindcss @tailwindcss/vite
Update `vite.config.ts` the same way, adding the Tailwind plugin and the `@/` alias that shadcn-vue’s generated component imports expect.
import path from "path"
import tailwindcss from "@tailwindcss/vite"
import vue from "@vitejs/plugin-vue"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [vue(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
Step 3: Initialize shadcn/ui in the React project
The React CLI’s package name changed from `shadcn-ui` to plain `shadcn` after the project’s August 2024 rename, and the old `shadcn-ui` npm package is now marked deprecated with a note telling developers it “is going to be deprecated soon.” Always run the current package name.
npx shadcn@latest init
The init wizard asks for a base color, whether to use CSS variables for theming, and confirms your `@/` alias. It writes a `components.json` config file and updates your global CSS with Tailwind’s theme tokens. With Tailwind v4, the CLI can now initialize projects directly against the new CSS-first config format instead of generating a legacy `tailwind.config.js`, according to the project’s own Tailwind v4 integration documentation.
Add your first components:
npx shadcn@latest add button dialog table switch
This drops four new files into `src/components/ui/`: `button.tsx`, `dialog.tsx`, `table.tsx`, and `switch.tsx`. Open any of them and you will see plain, readable React function components — no compiled bundle, no black box.
Step 4: Initialize shadcn-vue in the Vue project
shadcn-vue’s CLI mirrors the React flow closely, which is the whole point of the project — it exists specifically so Vue teams do not have to learn a second mental model.
npx shadcn-vue@latest init
One detail worth knowing before you run this: shadcn-vue’s changelog states that as of the Reka UI v2 release, `shadcn-vue@latest` installs components built on Reka UI by default. If an existing project still depends on the older Radix Vue primitives, the maintainers explicitly kept a `shadcn-vue@radix` command available so teams are not forced into an immediate migration.
# Default install (Reka UI primitives)
npx shadcn-vue@latest add button dialog table switch
# Legacy path, if you need to match an older Radix Vue project
npx shadcn-vue@radix add button dialog table switch
The Vue CLI also picked up a behavior change worth flagging for anyone upgrading an older project: initialization now updates your existing Tailwind configuration files instead of overwriting them outright, according to the shadcn-vue project’s own changelog. That single fix eliminated a class of merge-conflict complaints from teams who had already customized their Tailwind setup before running `init`.
Step 5: Build the button and dialog in React
With components installed, wire up a settings dialog. This is standard React: import the generated component, manage the open state with `useState`, and compose.
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
export function SettingsDialog() {
const [open, setOpen] = useState(false)
return (
Account settings
Manage your profile and notification preferences.
)
}
Step 6: Build the same dialog in Vue
The Vue version reads almost the same, but uses `ref` for state and `v-model` binding instead of controlled props, and slots instead of `asChild` composition.
<script setup lang="ts">
import { ref } from "vue"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
const open = ref(false)
</script>
<template>
<Dialog v-model:open="open">
<DialogTrigger as-child>
<Button variant="outline">Open settings</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Account settings</DialogTitle>
</DialogHeader>
<p class="text-sm text-muted-foreground">
Manage your profile and notification preferences.
</p>
</DialogContent>
</Dialog>
</template>
Functionally these are identical: same accessible dialog behavior (focus trapping, escape-to-close, ARIA roles), same visual output, same Tailwind classes. The difference is entirely syntactic — React’s JSX and hooks versus Vue’s template blocks and Composition API refs.
Step 7: Add a dark mode toggle
Both toolchains rely on Tailwind’s `dark:` variant, toggled by adding or removing a `dark` class on the `html` element. Neither shadcn/ui nor shadcn-vue ships a built-in theme provider out of the box for a plain Vite app — you wire it yourself, which is exactly the “own your code” philosophy in practice.
// React: src/hooks/use-theme.ts
import { useEffect, useState } from "react"
export function useTheme() {
const [theme, setTheme] = useState(
() => (localStorage.getItem("theme") as "light" | "dark") || "light"
)
useEffect(() => {
document.documentElement.classList.toggle("dark", theme === "dark")
localStorage.setItem("theme", theme)
}, [theme])
return { theme, setTheme }
}
// Vue: src/composables/useTheme.ts
import { ref, watch } from "vue"
const stored = (localStorage.getItem("theme") as "light" | "dark") || "light"
export const theme = ref(stored)
watch(theme, (value) => {
document.documentElement.classList.toggle("dark", value === "dark")
localStorage.setItem("theme", value)
}, { immediate: true })
Note the structural difference: React’s hook re-runs its effect on every render where `theme` changes, scoped per-component-instance. Vue’s composable exports a single module-level `ref`, so every component that imports `theme` shares the same reactive value without prop drilling or context providers. That is a meaningful architectural difference beyond syntax — Vue’s reactivity system makes simple global state trivially shareable, while React typically needs Context or a state library for the same result.
Step 8: Build a data table
Data tables are where the two ecosystems’ surrounding tooling diverges most. Neither shadcn/ui nor shadcn-vue’s `table` component includes sorting, filtering, or pagination logic — both are unstyled-behavior-plus-styled-markup, and both projects point developers toward pairing with a headless table engine.
// React: pairs with @tanstack/react-table
npm install @tanstack/react-table
// Vue: pairs with @tanstack/vue-table
npm install @tanstack/vue-table
TanStack Table ships framework-specific adapters for both, so the underlying column-definition API is nearly identical between the two projects — only the rendering layer (JSX rows vs. template `v-for` loops) differs. This is one of the few places where the ecosystems have converged on a shared solution rather than diverging.
Step 9: Theme customization and design tokens
Both projects use CSS variables for theming, defined in your global stylesheet and consumed through Tailwind’s `@theme` directive in v4. Editing the accent color, border radius, or font scale means editing these variables directly — there is no theme-provider prop API to configure, because there is no runtime theming layer standing between your CSS and the rendered component.
@import "tailwindcss";
@theme {
--color-primary: oklch(0.205 0 0);
--color-primary-foreground: oklch(0.985 0 0);
--radius: 0.625rem;
}
This file is shared verbatim between the React and Vue projects — Tailwind’s configuration is framework-agnostic, so this is one setup step you genuinely only do once if you are running both stacks side by side, for example in a design-system monorepo.
Step 10: Handling forms with validation
shadcn/ui’s `form` component is a thin wrapper documented to pair with React Hook Form and Zod for schema validation. shadcn-vue’s equivalent pairs with VeeValidate and Zod. Both add a `form` component via the CLI, which handles field-level error message rendering but delegates the actual validation logic to these external libraries.
# React
npx shadcn@latest add form
npm install react-hook-form @hookform/resolvers zod
# Vue
npx shadcn-vue@latest add form
npm install vee-validate @vee-validate/zod zod
Zod itself is shared between both stacks — one schema definition file can, in principle, validate data on both the React admin panel and the Vue customer portal in the same monorepo, which is a genuine advantage for teams running both frameworks concurrently during a migration.
Step 11: Registry blocks and pre-built pages
Beyond single components, both CLIs support installing entire pre-built page blocks — login screens, dashboards, pricing pages — from community registries. The shadcn-vue ecosystem specifically saw a dedicated blocks catalog site launch in August 2026, cataloged at over 1,400 Vue blocks built on shadcn-vue’s registry format, mirroring the block catalogs that have existed around shadcn/ui’s React registry for longer.
# Install a remote registry block by URL — supported by both CLIs
npx shadcn@latest add https://example.com/registry/dashboard-01.json
npx shadcn-vue@latest add https://example.com/registry/dashboard-01.json
The shadcn-vue changelog also notes the CLI can now install not just components but themes, composables, and utility functions through the same `add` command — a broader registry scope than a simple component-copy tool, and one that has closed most of the earlier feature gap with the React CLI’s v4 release.
Step 12: Testing your components
Because both libraries generate plain, ownable source files rather than importing from a compiled package, they test the same way the rest of your components do. React projects typically pair with Vitest and React Testing Library; Vue projects pair with Vitest and Vue Testing Library. Since the underlying accessibility behavior comes from Radix UI or Reka UI respectively, testing focuses on your composition and business logic rather than re-testing the primitive’s internals.
# Same test runner works for both stacks
npm install -D vitest @testing-library/react @testing-library/vue jsdom
Performance and reactivity differences worth understanding
Because neither shadcn/ui nor shadcn-vue ships its own rendering engine, the performance characteristics you get come entirely from React and Vue’s underlying reactivity models, not from the component library itself. That distinction matters more than most setup guides mention, because it shapes how you should structure state around these generated components.
React re-renders a component function whenever its state or props change, then reconciles the resulting virtual DOM tree against the previous one. A shadcn/ui `Dialog` wrapping several child components means that toggling `open` re-runs the parent’s render function and diffs the subtree, relying on React’s reconciliation to skip untouched DOM nodes. For most dialog, dropdown, and table use cases this overhead is invisible, but it becomes noticeable in large data tables with hundreds of interactive rows, which is why pairing shadcn/ui’s table primitive with TanStack Table’s row virtualization is common practice for anything beyond a few dozen visible rows.
Vue’s reactivity system, by contrast, tracks fine-grained dependencies through its proxy-based reactive objects. A shadcn-vue `Switch` bound to a single `ref` only triggers a re-render of the DOM nodes that actually read that ref during their last render pass, rather than re-running the entire parent component function. In practice this means Vue components built with shadcn-vue tend to need less manual optimization (memoization, `useMemo`-equivalents) to avoid unnecessary re-renders in deeply nested UI, since the framework’s compiler already narrows update scope at build time.
Neither difference is a reason to choose one framework over the other for typical CRUD dashboards, settings pages, or admin panels — the kind of UI both libraries are built for. It becomes relevant mainly in data-dense interfaces: real-time trading dashboards, large spreadsheet-like grids, or live-updating monitoring views, where React teams commonly reach for `React.memo`, `useMemo`, and virtualization earlier than Vue teams building the equivalent screen.
Icon libraries and customizing the default look
Both CLIs default to the Lucide icon set — Lucide React for shadcn/ui, Lucide Vue Next for shadcn-vue — chosen for its consistent stroke width and large, actively maintained icon catalog. You are not locked into Lucide, though switching requires a manual edit rather than a CLI flag in either project.
# Swap to a different icon library in either stack
npm uninstall lucide-react
npm install @tabler/icons-react
# Then update each generated component's icon imports manually,
# since neither CLI auto-migrates icon references
Color scheme customization works identically on both sides once you understand the CSS variable structure. The `init` wizard’s “base color” prompt (neutral, slate, zinc, stone, gray, or a few others depending on CLI version) simply picks a starting palette of CSS custom properties. You are free to hand-edit these after the fact — nothing in either project locks you to the initial choice, and doing so does not require re-running `init` or `add` for already-installed components, since the components reference the CSS variables by name rather than hardcoding color values.
Setting up continuous integration for both stacks
Since shadcn/ui and shadcn-vue both copy code directly into your repository rather than installing a versioned package, your CI pipeline never needs to run their CLIs at all after initial setup — a meaningful operational difference from a typical npm-installed component library. Once `npx shadcn@latest add button` has run once and the generated file is committed, your build pipeline treats `button.tsx` exactly like any other file you wrote by hand.
# .github/workflows/ci.yml — works the same for both projects
# since the CLI never runs in CI
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run build
- run: npm run test
This is a genuine operational advantage over runtime-dependency component libraries: there is no risk of a CI build pulling a different component version than what a developer tested locally, because the component code itself is checked into version control. The only dependency drift risk left is in the underlying primitive libraries (Radix UI or Reka UI) and Tailwind CSS, which you manage through normal `package.json` version pinning like any other dependency.
Common pitfalls when adopting shadcn/ui or shadcn-vue
These are the mistakes that show up most often in issue trackers and community forums for both projects.
- Running the deprecated `shadcn-ui` package on React. The old package name is officially marked deprecated with a warning it will stop being supported. Always run `npx shadcn@latest`, not `npx shadcn-ui@latest`, on new projects.
- Mixing Radix Vue and Reka UI components in the same shadcn-vue project. Since `shadcn-vue@latest` now defaults to Reka UI while `shadcn-vue@radix` still targets the older primitives, adding components with both commands in one project produces two incompatible primitive dependencies and duplicated bundle weight.
- Forgetting the `@/` path alias. Both CLIs generate imports like `@/components/ui/button`. If your `tsconfig.json` and `vite.config.ts` alias configs are missing or mismatched, every generated component fails to resolve on first build.
- Overwriting a customized `globals.css` on re-init. Older versions of both CLIs could stomp on manually edited CSS variables when re-running `init`. Always diff before confirming an overwrite prompt, and note that shadcn-vue’s changelog specifically calls out that init now updates rather than replaces Tailwind config files.
- Assuming component APIs are drop-in identical across frameworks. Prop names, slot conventions, and event names are not guaranteed to match one-to-one between the React and Vue versions of the same component, since they are maintained by separate teams against separate primitive libraries.
- Skipping Tailwind v4 migration before running the latest CLI. The current CLI generations are built and documented against Tailwind v4’s CSS-first configuration. Running them against a legacy Tailwind v3 `tailwind.config.js` setup can produce partially broken utility classes.
- Not pinning the CLI version in CI. Because these are “copy code in” tools rather than versioned runtime dependencies, running `npx shadcn@latest` in a CI pipeline months apart can pull in different component code each time. Pin to an explicit CLI version for reproducible builds.
Expected output when everything works
After Step 4, your Vue project’s `src/components/ui/` folder should contain something like this:
src/components/ui/
├── button/
│ ├── Button.vue
│ └── index.ts
├── dialog/
│ ├── Dialog.vue
│ ├── DialogContent.vue
│ ├── DialogHeader.vue
│ ├── DialogTitle.vue
│ ├── DialogTrigger.vue
│ └── index.ts
├── table/
│ └── (table primitives)
└── switch/
└── Switch.vue
Running `npm run dev` in either project should show a working button that opens an accessible modal dialog, dismissible with the Escape key, with focus correctly trapped inside the dialog and returned to the trigger button on close. If the dialog opens but the backdrop is unstyled or transparent, your Tailwind CSS import in the global stylesheet is not being picked up — check that `@import “tailwindcss”;` is the first line of your main CSS file.
Troubleshooting
- “Cannot find module ‘@/components/ui/button’” error. Your path alias is missing from either `tsconfig.json` (`paths` field) or `vite.config.ts` (`resolve.alias`). Both must match; the CLI checks `tsconfig.json` during init but Vite needs its own separate alias entry.
- Dialog opens with no visible overlay or backdrop. Tailwind’s CSS import order matters in v4. Confirm `@import “tailwindcss”;` sits above your `@theme` block, and that the CSS file is actually imported in your `main.tsx` or `main.ts` entry point.
- `npx shadcn-vue@latest add` fails with a Reka UI peer dependency warning. This usually means an existing component was installed under the older Radix Vue path. Remove the conflicting component folder and re-add it fresh under the current default.
- Dark mode class toggles but colors don’t change. Your `@theme` CSS variables likely aren’t declared inside a `.dark` selector override block. Both light and dark variable sets need explicit declarations; Tailwind does not auto-invert colors.
- TypeScript errors on generated `.tsx` or `.vue` files immediately after `add`. Confirm your TypeScript version matches what the CLI expects (5.4+) and that `@types/node` is installed, since several generated files reference Node path utilities.
- Build works locally but fails in CI with “module not found.” Case-sensitivity mismatch between your local filesystem (often case-insensitive on macOS) and your CI runner (case-sensitive on Linux). Check that imported file names match folder casing exactly.
- Old `shadcn-ui` CLI commands from a tutorial no longer work. Any guide referencing `npx shadcn-ui@latest init` is using the deprecated package name. Substitute `npx shadcn@latest init` for current projects.
- Components render unstyled with default browser styling. This typically means Tailwind’s content scanning isn’t picking up your `components/ui` directory. In Tailwind v4’s CSS-first setup, verify your `@source` directives (or automatic content detection) include the generated component paths.
- `shadcn-vue add` silently does nothing. Check you’re running the command from the project root where `components.json` exists — running it from a subfolder causes the CLI to fail to locate its config without always printing a clear error.
Advanced tips
Once the basics are running, a few patterns separate a quick demo from a production-ready setup on either stack.
First, treat `components.json` as a project-level contract, not a one-time init artifact. Both CLIs read it on every subsequent `add` command to determine your alias paths, styling approach, and icon library preference (Lucide is the default for both React and Vue variants). Committing this file and reviewing changes to it in code review catches drift before it causes import errors.
Second, if you are running a monorepo with both a React admin panel and a Vue customer app — a real and increasingly common scenario during framework migrations — centralize your Tailwind `@theme` tokens in a shared CSS package that both projects import. Since Tailwind’s configuration is framework-agnostic, this is one of the few places true design-system parity between shadcn/ui and shadcn-vue is achievable without duplicating work.
Third, for teams building internal design systems on top of either project, consider forking the generated components into a private, versioned internal package rather than re-running the CLI in every consuming app. This trades away some of the “own your code” flexibility for the consistency of a shared internal library — a reasonable tradeoff once you have more than three or four apps consuming the same components.
Fourth, when installing remote registry blocks from third-party sources, review the generated code before merging exactly as you would review a pull request from an external contributor. Because these tools copy code directly into your project rather than pulling a package through an audited registry like npm’s provenance system, there is no automatic dependency-scanning step protecting you from a malicious or outdated block.
Complete working project structure
Here is the final file layout for both completed projects, ready to extend with additional pages and components.
react-shadcn-demo/
├── components.json
├── vite.config.ts
├── tsconfig.json
├── src/
│ ├── main.tsx
│ ├── index.css
│ ├── hooks/
│ │ └── use-theme.ts
│ └── components/
│ ├── SettingsDialog.tsx
│ └── ui/
│ ├── button.tsx
│ ├── dialog.tsx
│ ├── table.tsx
│ ├── switch.tsx
│ └── form.tsx
vue-shadcn-demo/
├── components.json
├── vite.config.ts
├── tsconfig.json
├── src/
│ ├── main.ts
│ ├── index.css
│ ├── composables/
│ │ └── useTheme.ts
│ └── components/
│ ├── SettingsDialog.vue
│ └── ui/
│ ├── button/
│ ├── dialog/
│ ├── table/
│ ├── switch/
│ └── form/
React vs Vue: which one fits your project
The honest answer is that this decision should almost never be driven by shadcn/ui versus shadcn-vue on their own — you pick React or Vue for your project first, based on team experience, existing codebase, and hiring pool, and the component tooling follows from that choice. Both projects have reached functional parity on the fundamentals: CLI-based component copying, Tailwind CSS styling, CSS-variable-based theming, and a shared philosophy of ownership over abstraction.
Where they still differ meaningfully: shadcn/ui’s React ecosystem has a longer track record, a larger surrounding community of registries and blocks, and tighter alignment with Next.js’s App Router conventions. shadcn-vue is younger, moved through a primitive-library transition from Radix Vue to Reka UI more recently, and its blocks ecosystem — while growing quickly, evidenced by the August 2026 launch of a dedicated 1,400-block Vue catalog — is still catching up in raw volume. Neither gap is a reason to pick a frontend framework you would not otherwise choose.
| Factor | shadcn/ui (React) | shadcn-vue (Vue) |
|---|---|---|
| Underlying primitives | Radix UI (React) | Reka UI (default) or Radix Vue (legacy path) |
| Current CLI | shadcn v4 (package `shadcn`) | shadcn-vue 2.4.3 |
| Styling engine | Tailwind CSS v4 | Tailwind CSS v4 |
| Form validation pairing | React Hook Form + Zod | VeeValidate + Zod |
| Table pairing | @tanstack/react-table | @tanstack/vue-table |
| Icon library default | Lucide React | Lucide Vue Next |
| Meta-framework fit | Next.js (App Router) | Nuxt 4 |
| Blocks/registry ecosystem | Larger, longer-established | Growing fast, newer (1,400+ blocks catalog launched Aug 2026) |
Migrating an existing component library
If you are moving off an older UI kit like Material UI, Bootstrap, or Element Plus toward either of these projects, do it incrementally rather than in one pass. Because both CLIs generate standalone component files rather than a monolithic package, you can install shadcn/ui or shadcn-vue components page by page, letting old and new components coexist during the transition. Start with low-traffic internal tools or admin panels before touching customer-facing checkout or authentication flows, since those are the screens where an accessibility regression from a primitive-library swap would do the most damage.
Run your existing accessibility test suite (axe-core is common on both stacks) against the newly generated components before removing the old library entirely. Radix UI and Reka UI both have strong accessibility track records, but a swapped primitive library is still a behavioral change worth verifying rather than assuming.
Accessibility considerations when swapping primitive libraries
The accessibility guarantees behind both projects come from Radix UI and Reka UI, not from shadcn/ui or shadcn-vue themselves. Both primitive libraries implement WAI-ARIA authoring practices for the components they ship: correct roles, keyboard navigation, focus management, and screen-reader announcements are built into the primitive, not something you add on top after generating the styled wrapper.
This matters most during the Radix Vue to Reka UI transition on the Vue side. Because Reka UI is a continuation of the same codebase rather than a rewrite from scratch, the accessibility behavior should carry over largely unchanged for components that existed in both versions. Still, any project migrating an older shadcn-vue install from the Radix Vue path to the current Reka UI default should re-run its accessibility test suite rather than assume parity, particularly around components added or significantly changed since the transition.
# Automated accessibility checks, same tool for both stacks
npm install -D @axe-core/react # React
npm install -D vue-axe # Vue
Automated tools like axe-core catch a meaningful chunk of accessibility issues (missing labels, insufficient color contrast, incorrect heading order) but cannot verify screen-reader announcement quality or keyboard-only usability end to end. For any component handling critical flows — checkout, account settings, authentication — pair automated scanning with at least one manual pass using a screen reader (VoiceOver on macOS, NVDA on Windows) before shipping to production.
Package.json reference for both projects
For reference, here is what a minimal `package.json` dependency block looks like after completing every step in this tutorial, useful for diffing against your own setup if something isn’t resolving correctly.
// react-shadcn-demo/package.json (dependencies excerpt)
{
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0",
"@radix-ui/react-dialog": "^1.1.0",
"react-hook-form": "^7.53.0",
"@hookform/resolvers": "^3.9.0",
"zod": "^3.23.0",
"@tanstack/react-table": "^8.20.0",
"lucide-react": "^0.460.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.5.0"
},
"devDependencies": {
"tailwindcss": "^4.3.0",
"@tailwindcss/vite": "^4.3.0",
"typescript": "^5.6.0",
"vite": "^8.0.0"
}
}
// vue-shadcn-demo/package.json (dependencies excerpt)
{
"dependencies": {
"vue": "^3.5.0",
"reka-ui": "^2.0.0",
"vee-validate": "^4.13.0",
"@vee-validate/zod": "^4.13.0",
"zod": "^3.23.0",
"@tanstack/vue-table": "^8.20.0",
"lucide-vue-next": "^0.460.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.5.0"
},
"devDependencies": {
"tailwindcss": "^4.3.0",
"@tailwindcss/vite": "^4.3.0",
"typescript": "^5.6.0",
"vite": "^8.0.0"
}
}
Both dependency trees are noticeably shallow compared to a traditional component library import, since the bulk of the “library” is source code already sitting in your `src/components/ui` folder rather than compiled code in `node_modules`. Auditing these two files side by side is a fast way to confirm your project matches the versions used throughout this tutorial before filing a bug report against either CLI.
Frequently asked questions
Is shadcn-vue an official Vue port maintained by the shadcn/ui team?
No. shadcn-vue is a community-driven project maintained separately, under the `unovue` GitHub organization, that ports the same design system and CLI philosophy to Vue. It is not officially affiliated with the original shadcn/ui React project, though it closely mirrors its conventions.
Do I need Tailwind CSS to use either library?
Yes, in practice. Both projects generate components styled with Tailwind utility classes and CSS variables. While it is technically possible to strip Tailwind out and hand-write equivalent CSS, neither CLI supports that as a configuration option, so you would be maintaining the substitution yourself indefinitely.
Can I use shadcn-vue components inside a Nuxt project instead of plain Vite?
Yes. shadcn-vue supports Nuxt 4 as a target framework, and the CLI detects a Nuxt project structure during init to adjust its generated import paths and module registration accordingly.
What happened to Radix Vue — is it deprecated?
Radix Vue was rebranded and continued as Reka UI, which its own repository describes as the same project “previously Radix Vue.” shadcn-vue’s default registry moved to Reka UI following that transition, though a `shadcn-vue@radix` CLI path remains available for projects that have not migrated.
Which is faster to build with, shadcn/ui or shadcn-vue?
There is no published, controlled benchmark comparing developer velocity between the two. In practice, the deciding factor is almost always which framework your team already knows well, since both CLIs have converged on nearly identical command syntax and workflow.
Do these components work with server-side rendering?
Yes. shadcn/ui’s generated components work with Next.js server and client component boundaries (marking interactive components with `”use client”` where needed), and shadcn-vue’s components work with Nuxt’s universal rendering, since both underlying primitive libraries (Radix UI and Reka UI) are designed with SSR compatibility in mind.
Can I mix shadcn/ui components with a different component library in the same React project?
Technically yes, since each generated component is just plain code with no special runtime requirement, but doing so long-term usually creates visual and behavioral inconsistency. Most teams treat the CLI-generated components as the base layer and layer additional one-off components on top using the same Radix UI and Tailwind conventions, rather than mixing in an entirely separate design system.
Is there a cost to using either library?
No. Both shadcn/ui and shadcn-vue are open source and free to use, including for commercial projects. There is no paid tier, license fee, or usage cap on either CLI or the components it generates.