iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🪴

Step-by-Step Guide to Setting Up Vite + Vue 3 + Tailwind CSS v4 + shadcn-vue

に公開

Official Documentation

https://www.shadcn-vue.com/docs/installation/vite

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.

https://github.com/unovue/shadcn-vue/issues/1795

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
setup.sh
#!/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