Vue With Vite, Tailwind CSS and PrimeVue
Set up a Vue 3 project with TypeScript and Vite by hand, file by file, then add Tailwind CSS and PrimeVue and make them share their colors and dark mode.

Quest objectives
- Set up a Vue 3 project with Vite and TypeScript by hand
- Type-check .vue files with vue-tsc
- Add Tailwind CSS 4 through its Vite plugin
- Add PrimeVue with a theme in your own color
- Make Tailwind and PrimeVue share colors, CSS layers and dark mode
A project generator sets up Vue in one command, but leaves you with a dozen files you didn't choose. Built by hand, the project has only what it needs, and you know what every file is for. This tutorial builds a Vue 3 project with TypeScript and Vite step by step, adds Tailwind CSS and then the component library PrimeVue, and makes the two work together.
You need Node.js 20.19, 22.12 or newer, which is what Vite 8 runs on. The commands use npm. With pnpm or Yarn, write pnpm add or yarn add where it says npm install.
The project
A folder and a package.json are all a project needs to begin with:
$ mkdir my-app$ cd my-app
{
"name": "my-app",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --build && vite build",
"preview": "vite preview"
}
}"type": "module" makes Node treat the project's JavaScript as ES modules. The scripts are the three you will use: dev starts the development server, build type-checks the project and then builds it for production, and preview serves that build.
Now the packages. Vue is needed at runtime, everything else only while you develop:
$ npm install vue$ npm install -D vite @vitejs/plugin-vue typescript@~6.0 vue-tsc @vue/tsconfig @tsconfig/node24 @types/node@24
vite is the development server and build tool, and @vitejs/plugin-vue teaches it single-file components.
vue-tsc type-checks
.vuefiles, which plaintsccan't read.@vue/tsconfig and @tsconfig/node24 are ready-made TypeScript settings for the browser and for Node, and @types/node describes Node's APIs for
vite.config.ts.
TypeScript
The project has code for two environments: the app runs in the browser, and vite.config.ts runs in Node. Each gets its own configuration, and a third file ties them together:
{
"files": [],
"references": [
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.app.json" }
]
}{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["src/**/*.ts", "src/**/*.vue"],
"compilerOptions": {
"types": ["vite/client"],
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
}
}{
"extends": "@tsconfig/node24/tsconfig.json",
"include": ["vite.config.ts"],
"compilerOptions": {
"module": "preserve",
"moduleResolution": "bundler",
"types": ["node"],
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
}
}vue-tsc --build follows the references in tsconfig.json and checks both parts. For the app:
@vue/tsconfig brings the strict settings Vue recommends, including
strictitself.vite/clientadds Vite's types, so TypeScript knowsimport.meta.envand accepts imports of CSS and images.noUncheckedIndexedAccesstreatslist[0]as possiblyundefined, which it is for an empty list.noUnusedLocalsandnoUnusedParameterspoint out code that does nothing.
The .tsbuildinfo files remember what was checked, so the next check only looks at what changed. They go into node_modules/.tmp, out of the way.
Vite and the entry point
Vite needs one plugin for now:
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [vue()],
});The page itself is index.html in the project's root. Vite starts from it and follows the script tag to src/main.ts:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My app</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');<script setup lang="ts">
const name = 'app';
</script>
<template>
<h1>Hello {{ name }}</h1>
</template>Start the development server:
$ npm run dev VITE v8.3.5 ready in 312 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ press h + enter to show help
Open http://localhost:5173/, and "Hello app" is there. Leave the server running: from now on, every change shows up in the browser as soon as you save.
Tailwind CSS
Tailwind 4 comes as a Vite plugin and needs neither a tailwind.config.js nor PostCSS:
$ npm install -D tailwindcss @tailwindcss/viteimport tailwindcss from '@tailwindcss/vite';
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [vue(), tailwindcss()],
});A stylesheet imports Tailwind. Its settings, such as your own colors, go into the same file with @theme when you need them:
@import 'tailwindcss';main.ts imports the stylesheet before anything else:
import './assets/main.css';
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');To see that it works, give the heading a few classes:
<script setup lang="ts">
const name = 'app';
</script>
<template>
<h1 class="p-8 text-3xl font-bold text-orange-500">Hello {{ name }}</h1>
</template>Vite notices the changed configuration and restarts on its own. The heading turns big, bold and orange.
PrimeVue
PrimeVue brings ready-made components: buttons, tables, dialogs, date pickers and about 80 more, with themes you can adapt.
$ npm install primevue@4 @primeuix/themes@2$ npm install -D tailwindcss-primeui
@primeuix/themes holds PrimeVue's themes, and tailwindcss-primeui makes PrimeVue's colors available as Tailwind classes. It goes into the stylesheet, together with a dark variant that matches PrimeVue's:
@import 'tailwindcss';
@import 'tailwindcss-primeui';
/* dark: applies inside an element with the class dark, the same selector PrimeVue uses */
@custom-variant dark (&:where(.dark, .dark *));Tailwind's dark: classes normally follow the system setting. With the custom variant, they apply inside an element with the class dark instead, so a switch on the page can turn dark mode on and off.
PrimeVue's own setup goes into a file of its own, so main.ts stays short:
import { definePreset } from '@primeuix/themes';
import Aura from '@primeuix/themes/aura';
import PrimeVue from 'primevue/config';
import ToastService from 'primevue/toastservice';
import type { App } from 'vue';
// Aura, with orange instead of emerald as the primary color
const Preset = definePreset(Aura, {
semantic: {
primary: {
50: '{orange.50}',
100: '{orange.100}',
200: '{orange.200}',
300: '{orange.300}',
400: '{orange.400}',
500: '{orange.500}',
600: '{orange.600}',
700: '{orange.700}',
800: '{orange.800}',
900: '{orange.900}',
950: '{orange.950}',
},
},
});
export function installPrimeVue(app: App) {
app.use(PrimeVue, {
ripple: true,
theme: {
preset: Preset,
options: {
darkModeSelector: '.dark',
// Puts PrimeVue's styles between Tailwind's base and utilities,
// so a utility class on a component wins
cssLayer: {
name: 'primevue',
order: 'theme, base, primevue',
},
},
},
});
// For toast messages, through useToast()
app.use(ToastService);
}definePresettakes the Aura theme and replaces its primary color.{orange.500}refers to a color of the theme's palette, so all eleven shades fit together.darkModeSelector: '.dark'is the same selector as the custom variant in the stylesheet: one class on<html>switches both.cssLayerputs PrimeVue's styles into a CSS layer of its own, between Tailwind's base styles and its utilities. A Tailwind class on a PrimeVue component therefore wins, without!important.rippleturns on the ripple effect when a button is clicked, and theToastServicelets any component show toast messages.
main.ts calls it before mounting the app:
import './assets/main.css';
import { createApp } from 'vue';
import App from './App.vue';
import { installPrimeVue } from './primevue';
const app = createApp(App);
installPrimeVue(app);
app.mount('#app');And App.vue tries it all out: two PrimeVue buttons, a toast and a dark mode switch, with Tailwind classes in between:
<script setup lang="ts">
import Button from 'primevue/button';
import Toast from 'primevue/toast';
import { useToast } from 'primevue/usetoast';
const toast = useToast();
function greet() {
toast.add({ severity: 'success', summary: 'Hello', detail: 'PrimeVue works.', life: 3000 });
}
function toggleDarkMode() {
document.documentElement.classList.toggle('dark');
}
</script>
<template>
<Toast />
<main class="min-h-screen bg-surface-0 p-8 text-surface-900 dark:bg-surface-950 dark:text-surface-0">
<h1 class="text-3xl font-bold text-primary">Hello app</h1>
<p class="mt-2 text-muted-color">Tailwind and PrimeVue share their colors and dark mode.</p>
<div class="mt-6 flex gap-2">
<Button label="Say hello" @click="greet" />
<Button label="Dark mode" severity="secondary" class="px-6" @click="toggleDarkMode" />
</div>
</main>
</template>Components are imported one by one from their own path, such as primevue/button, so the build only contains the ones you use. In the browser:
The heading and the first button are orange.
text-primary,text-muted-colorand thesurfacecolors come fromtailwindcss-primeuiand are the theme's own colors.The second button is wider than the first: Tailwind's
px-6beats PrimeVue's own padding, thanks to the layer order.Say hello shows a toast in the top right corner.
Dark mode puts the class
darkon<html>. The page turns dark through Tailwind'sdark:classes, and PrimeVue's buttons switch to their dark colors at the same time.
Building for production
$ npm run build$ npm run preview
npm run build type-checks the project and writes the finished site to dist, and npm run preview serves it at http://localhost:4173/ to try. The project now looks like this, next to node_modules:
my-app/
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.app.json
├── tsconfig.node.json
├── vite.config.ts
└── src/
├── App.vue
├── main.ts
├── primevue.ts
└── assets/
└── main.cssWith PrimeVue 5
With a license key for PrimeVue 5, install primevue@5 and @primeuix/themes@3 instead and add the key to the options in src/primevue.ts. Everything else stays as it is:
app.use(PrimeVue, {
license: 'your license key',
theme: {
// ...as above
},
});From here, Your First Vue 3 Composables is a good next step.