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

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
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!