iTranslated by AI
Step-by-Step Guide to Setting Up Vite + Vue 3 + Tailwind CSS v4 + shadcn-vue
Official Documentation
Basically, I was able to proceed by following the official documentation, but there were a few stumbling blocks along the way.
Environment
node -v
v24.15.0
pnpm -v
11.1.3
Create Project
pnpm create vite@latest my-app --template vue-ts --no-interactive
Verification
cd my-app
pnpm install
pnpm dev --open
Once you have confirmed that it is working, terminate the process and proceed to the next step.
Installing Tailwind CSS v4
pnpm add tailwindcss @tailwindcss/vite
After confirming that version 4 is installed, proceed to the next step.
Rewriting src/style.css
cat <<'EOF' > src/style.css
@import "tailwindcss";
EOF
Leave only this single line.
One might think it could detect that Tailwind CSS v4 is already installed, but if you do not complete this edit, the pnpm dlx shadcn-vue@latest init command run later will result in a validation error.
Adding Aliases to tsconfig.json
cat <<'EOF' > tsconfig.json
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" },
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
},
},
}
EOF
Add the entire compilerOptions section.
It seems that this JSON file does not throw an error even if there is a trailing comma at the end of the data. How is one supposed to distinguish between files that allow trailing commas and those that do not?
Adding the Same Content to tsconfig.app.json
cat <<'EOF' > tsconfig.app.json
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
/* From here */
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*",
],
},
/* Added until here */
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
}
EOF
Add the baseUrl and paths sections.
The fact that you have to write the same content in two places makes the design seem disjointed.
Adding Aliases to vite.config.ts as Well
First, install @types/node.
pnpm add -D @types/node
This is for import path from 'node:path'.
Once installed, set the file content as follows:
cat <<'EOF' > vite.config.ts
import path from 'node:path'
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
EOF
Why the Method Using vite-tsconfig-paths Doesn't Work
There is also a method using vite-tsconfig-paths described in the official documentation.
Method using vite-tsconfig-paths
pnpm add -D vite-tsconfig-paths
cat <<'EOF' > vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
plugins: [vue(), tailwindcss(), tsconfigPaths()],
})
EOF
However, for some reason, @ cannot be resolved. It was working initially, but stopped working after I tried various things. Why???
Pre-approving vue-demi and msw
cat <<'EOF' > pnpm-workspace.yaml
allowBuilds:
vue-demi: true
msw: true
EOF
I found out later that doing this beforehand prevents the shadcn-vue command from being interrupted.
Installing shadcn-vue
If you follow the official documentation and run:
pnpm dlx shadcn-vue@latest init
It throws an error, leaving me stuck.
The cause was a bug in vue-metamorph, which shadcn-vue depends on. It seems the issue is now avoided by pinning the version it references.
Also, since it only asks about the vue-demi package when using pnpm, select Yes when prompted. vue-demi appears to be a library that bridges the differences between Vue 2 and 3.
If you need to retry the execution, it seems you can just delete components.json.
I'm not entirely sure what the options mean, but I answered as follows:
- ✔ Which component library would you like to use? › Reka UI
- ✔ Which icon library would you like to use? › Lucide
- ✔ Which font would you like to use? › Inter
- ✔ Which color would you like to use as the base color? › Neutral
Importing the button Component
Following the official documentation, running:
pnpm dlx shadcn-vue@latest add button
results in the following error:
- ✔ Checking registry.
- ✔ Updating CSS variables in src/style.css
Something went wrong. Please check the error below for more details.
If the problem persists, please open an issue on GitHub.
corepack pnpm add reka-ui failed.
Progress: resolved 1, reused 0, downloaded 0, added 0
Progress: resolved 125, reused 90, downloaded 0, added 0
Packages: +19
+++++++++++++++++++
Progress: resolved 127, reused 92, downloaded 0, added 19, done
dependencies:
+ reka-ui 2.9.7
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: vue-demi@0.14.10
Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.
This is due to pnpm's safety-first design, asking whether vue-demi is allowed to run build scripts. That said, even though I answered Yes to vue-demi during init, it is strange that it asks again. There are many things I don't understand, but for now, run:
pnpm approve-builds
Approve it and run the command again. If you see the following, you can proceed:
✔ Checking registry.
✔ Updating CSS variables in src/style.css
✔ Installing dependencies.
✔ Created 2 files:
- src/components/ui/button/Button.vue
- src/components/ui/button/index.ts
I learned later that you can avoid this interruption if you pre-allow vue-demi in pnpm-workspace.yaml.
Using the Component
cat <<'EOF' > src/components/HelloWorld.vue
<script setup lang="ts">
import { Button } from '@/components/ui/button'
</script>
<template>
<Button variant="destructive">Red Button</Button>
</template>
EOF
Verification
pnpm dev --open
If the red button appears, you are done.

Semi-automated Script
Open
#!/bin/sh -ve
node -v
pnpm -v
npm -v
rm -fr my-app
pnpm create vite@latest my-app --template vue-ts --no-interactive
cd my-app
pnpm install
pnpm add tailwindcss @tailwindcss/vite
cat <<'EOF' > src/style.css
@import "tailwindcss";
EOF
cat <<'EOF' > tsconfig.json
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" },
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
},
},
}
EOF
cat <<'EOF' > tsconfig.app.json
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
/* From here */
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*",
],
},
/* Added until here */
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
}
EOF
# pnpm add -D vite-tsconfig-paths
#
# cat <<'EOF' > vite.config.ts
# import { defineConfig } from 'vite'
# import vue from '@vitejs/plugin-vue'
# import tailwindcss from '@tailwindcss/vite'
# import tsconfigPaths from 'vite-tsconfig-paths'
#
# export default defineConfig({
# plugins: [vue(), tailwindcss(), tsconfigPaths()],
# })
# EOF
pnpm add -D @types/node
cat <<'EOF' > vite.config.ts
import path from 'node:path'
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
EOF
cat <<'EOF' > pnpm-workspace.yaml
allowBuilds:
vue-demi: true
msw: true
EOF
pnpm dlx shadcn-vue@latest init
pnpm dlx shadcn-vue@latest add button
cat <<'EOF' > src/components/HelloWorld.vue
<script setup lang="ts">
import { Button } from '@/components/ui/button'
</script>
<template>
<Button variant="destructive">Red Button</Button>
</template>
EOF
pnpm dev --open
Discussion