How to Set Up a Production Ready Vue 3 Project with Tailwind CSS 4 and Pinia in 2025

In this tutorial I'll show you how to create a robust and modular Vue project
Make sure you have the latest version of Node.js and npm.
Install Vue with Vite
npm create vite@latest my-app -- --template vue
Follow the prompts and select:
- Vue 3
- TypeScript (recommended)
- ESLint / Prettier (recommended)
Move into the project folder:
cd my-app
Install dependencies:
npm install
📁 Recommended Vue 3 + Tailwind + Pinia Project Structure
src/
│
├── api/ # API endpoint definitions and raw queries/mutations
│ └── auth.ts # e.g. loginUser, registerUser, etc.
│
├── components/ # Reusable UI components
│ ├── ui/ # Generic design components (Button, Modal, Input)
│ └── app/ # App-specific components (e.g. UserCard, QuizTile)
│
├── config/ # App-wide config and environment setup
│ └── env.ts # Typed access to import.meta.env
│
├── styles/ # Tailwind styles
│ └── main.css # Entrypoint of all styles
│
├── enums/ # Type-safe enumerators
│ └── userRoles.ts
│
├── localization/ # i18n messages and language handlers
│ ├── index.ts # i18n setup
│ └── en.ts
│
├── router/ # Vue Router setup and routes
│ ├── index.ts
│ └── guards/ # Navigation guards
│
├── services/ # GraphQL / REST client services
│ ├── apollo.ts # Apollo Client setup
│ ├── restClient.ts # Axios instance and REST abstractions
│ └── userService.ts
│
├── stores/ # Pinia stores (modular per domain)
│ ├── userStore.ts
│ └── gameStore.ts
│
├── utils/ # Utility functions
│ ├── cn.ts # Tailwind class combiner
│ ├── formatDate.ts
│ └── getAvatar.ts
│
├── validations/ # Front + backend-integrated validations
│ ├── authValidation.ts
│ └── serverValidator.ts
│
├── views/ # Route-level page components
│ ├── HomeView.vue
│ └── ProfileView.vue
│
├── App.vue
└── main.ts
🎨 Step 2: Install Tailwind CSS
Use the official Tailwind CLI integration:
npm install tailwindcss @tailwindcss/vite @tailwindcss/typography
🧰 Step 3: Configure Tailwind
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), tailwindcss()],
})
vite.config.ts
💅 Step 4: Add Tailwind to Your CSS
Create a src/styles/colors.css
@theme {
--color-primary-50: #f2f3fc;
--color-primary-100: #e2e4f7;
--color-primary-200: #cbcef2;
--color-primary-300: #a7afe9;
--color-primary-400: #7e87dc;
--color-primary-500: #5f63d2;
--color-primary-600: #504bc5;
--color-primary-700: #4b41b4;
--color-primary-800: #443a93;
--color-primary-900: #302b63;
--color-primary-950: #282348;
--color-primary: #282348;
--color-secondary-50: #fef1f9;
--color-secondary-100: #fee5f5;
--color-secondary-200: #ffcbed;
--color-secondary-300: #ffa1dc;
--color-secondary-400: #ff6ec4;
--color-secondary-500: #fa3aa6;
--color-secondary-600: #ea1884;
--color-secondary-700: #cc0a69;
--color-secondary-800: #a80c56;
--color-secondary-900: #8c0f4a;
--color-secondary-950: #560129;
--color-secondary: #560129;
--color-success-50: #ecfdf7;
--color-success-100: #d1faec;
--color-success-200: #a7f3da;
--color-success-300: #6ee7bf;
--color-success-400: #34d39e;
--color-success-500: #10b981;
--color-success-600: #059666;
--color-success-700: #047852;
--color-success-800: #065f42;
--color-success-900: #064e36;
--color-success-950: #022c1e;
--color-danger-50: #fef2f2;
--color-danger-100: #fee2e2;
--color-danger-200: #fecaca;
--color-danger-300: #fca5a5;
--color-danger-400: #f87171;
--color-danger-500: #ef4444;
--color-danger-600: #dc2626;
--color-danger-700: #b91c1c;
--color-danger-800: #991b1b;
--color-danger-900: #7f1d1d;
--color-danger-950: #450a0a;
--color-gray-50: #f6f5f5;
--color-gray-100: #e7e6e6;
--color-gray-200: #d2d0cf;
--color-gray-300: #b3b0ad;
--color-gray-400: #88837f;
--color-gray-500: #716c69;
--color-gray-600: #615d59;
--color-gray-700: #524f4c;
--color-gray-800: #474543;
--color-gray-900: #3e3d3b;
--color-gray-950: #272625;
--color-black: #000000;
--color-black-50: #c8c8c8;
--color-black-100: #b4b4b4;
--color-black-200: #969696;
--color-black-300: #828282;
--color-black-400: #646464;
--color-black-500: #464646;
--color-black-600: #323232;
--color-black-700: #282828;
--color-black-800: #1e1e1e;
--color-black-900: #141414;
}
src/styles/colors.css
Create a src/styles/typography.css
@theme {
--font-*: initial;
--font-poppins: "Poppins", "sans-serif";
--font-merriweather: "Merriweather", "serif";
--font-fira-code: "Fira Code", "monospaced";
--font-sans: var(--font-poppins);
--font-serif: var(--font-merriweather);
--font-mono: var(--font-fira-code);
--text-xxs: 0.65rem;
--text-xs: 0.7rem;
--text-sm: 0.85rem;
--text-base: 1rem;
--text-lg: 1.5rem;
--text-xl: 2rem;
--text-2xl: 3rem;
--text-2\.5xl: 4rem;
}
src/styles/typography.css
Create a src/styles/main.css file
@layer theme, base, utilities, preflight;
/* Tailwind */
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
@import "./typography.css";
@import "./colors.css";
@plugin '@tailwindcss/typography';
@theme {
--spacing: 8px;
}
@layer theme {
div[data-reka-popper-content-wrapper] {
z-index: 10 !important;
}
body {
@apply bg-black;
@apply m-0;
}
}
@layer utilities {
@keyframes slide-down {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(0, -0.5rem, 0);
}
}
@keyframes slide-up {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(0, -0.5rem, 0);
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes slide-down-and-fade-in {
0% {
opacity: 0;
transform: translate3d(0, -1rem, 0);
}
100% {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes slide-up-and-fade-out {
0% {
opacity: 1;
transform: translate3d(0, 0, 0);
}
100% {
opacity: 0;
transform: translate3d(0, -1rem, 0);
}
}
@keyframes slide-up-and-fade-in {
0% {
opacity: 0;
transform: translate3d(0, 1rem, 0);
}
100% {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes slide-down-and-fade-out {
0% {
opacity: 1;
transform: translate3d(0, 0, 0);
}
100% {
opacity: 0;
transform: translate3d(0, 1rem, 0);
}
}
@keyframes slide-left-and-fade-in {
0% {
opacity: 0;
transform: translate3d(1rem, 0, 0);
}
100% {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes slide-right-and-fade-out {
0% {
opacity: 1;
transform: translate3d(0, 0, 0);
}
100% {
opacity: 0;
transform: translate3d(1rem, 0, 0);
}
}
@keyframes slide-right-and-fade-in {
0% {
opacity: 0;
transform: translate3d(-1rem, 0, 0);
}
100% {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes slide-left-and-fade-out {
0% {
opacity: 1;
transform: translate3d(0, 0, 0);
}
100% {
opacity: 0;
transform: translate3d(-1rem, 0, 0);
}
}
@keyframes scale-in {
0% {
opacity: 0;
transform: translate3d(-50%, -48%) scale(0.96);
}
100% {
opacity: 1;
transform: translate3d(-50%, -50%) scale(1);
}
}
}
@utility animate-scale-in {
animation: scale-in 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
@utility animate-slide-down {
animation: slide-down 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@utility animate-fade-in {
animation: fade-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@utility animate-fade-out {
animation: fade-out 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@utility animate-slide-up {
animation: slide-up 0.3s;
}
@utility animate-slide-down-and-fade-in {
animation: slide-down-and-fade-in 0.2s;
}
@utility animate-slide-up-and-fade-out {
animation: slide-up-and-fade-out 0.2s;
}
@utility animate-slide-up-and-fade-in {
animation: slide-up-and-fade-in 0.2s;
}
@utility animate-slide-down-and-fade-out {
animation: slide-down-and-fade-out 0.2s;
}
@utility animate-slide-left-and-fade-in {
animation: slide-left-and-fade-in 0.2s;
}
@utility animate-slide-right-and-fade-out {
animation: slide-right-and-fade-out 0.2s;
}
@utility animate-slide-right-and-fade-in {
animation: slide-right-and-fade-in 0.2s;
}
@utility animate-slide-left-and-fade-out {
animation: slide-left-and-fade-out 0.2s;
}
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity duration-300;
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
.fade-enter-to,
.fade-leave-from {
@apply opacity-100;
}
.scale-fade-enter-active,
.scale-fade-leave-active {
@apply transition-all duration-250;
}
.scale-fade-enter-from {
@apply opacity-0 scale-95;
}
.scale-fade-enter-to {
@apply opacity-100 scale-100;
}
.scale-fade-leave-from {
@apply opacity-100 scale-100;
}
.scale-fade-leave-to {
@apply opacity-0 scale-95;
}
.slide-up-enter-active {
transition: all 0.3s ease-out;
}
.slide-up-leave-active {
transition: all 0.2s ease-in;
}
.slide-up-enter-from {
transform: translateY(20px);
opacity: 0;
}
.slide-up-enter-to {
transform: translateY(0);
opacity: 1;
}
.slide-up-leave-from {
opacity: 1;
}
.slide-up-leave-to {
opacity: 0;
}
.pop-enter-from {
opacity: 0;
transform: translateY(100%) scale(0.95);
}
.pop-leave-to {
opacity: 0;
transform: translateY(100%) scale(0.95);
}
.pop-enter-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.pop-leave-from {
opacity: 1;
transform: translateY(0%);
}
.pop-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.pop-leave-to {
opacity: 0;
transform: translateY(100%);
}
src/styles/main.css
Add this into the index.html
<head>
<link href="/src/styles/main.css" rel="stylesheet">
</head>
🧪 Step 5: Test it!
Try using tailwind classes such as text-gray-500 or bg-gray-500 on any element!
🧼 Optional: Add Prettier + Tailwind Plugin
Install prettier and eslint
npm i -D @vue/eslint-config-prettier @vue/eslint-config-typescript prettier eslint eslint-plugin-vue
Add this into eslint.config.ts
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
rules: {
'vue/component-name-in-template-casing': [
'error',
'kebab-case',
{
registeredComponentsOnly: true,
ignores: [],
},
],
},
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
skipFormatting,
)
eslint.config.ts
Add this into the .prettierrc.json
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}
.prettierrc.json
Add these scripts into the package.json
"scripts: {
...
"lint": "eslint . --fix",
"format": "prettier --write src/"
}
package.json
Run those commands! 😁
🧠 Step 6: Add Pinia for State Management
npm install pinia
import { defineStore } from 'pinia'
export const useUserStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
import { createPinia } from 'pinia'
import App from './App.vue'
...
const app = createApp(App)
const store = createPinia()
app.use(store)
✅ Summary
You now have a blazing-fast Vue 3 setup with:
- Tailwind CSS for beautiful styles
- Pinia for clean state management
- Vite for instant hot reload and builds
Ready for serious app development.
Would you like me to create another post where I'd cover implementing connecting to services such as REST and ApolloGQL with Vue-Router and i18n? Let me know!