---
name: vue-best-practices
description: Vue.js 3 best practices guidelines covering Composition API, component design, reactivity patterns, Tailwind CSS utility-first styling, PrimeVue component library integration, and code organization. This skill should be used when writing, reviewing, or refactoring Vue.js code to ensure idiomatic patterns and maintainable code.
license: MIT
---
# Vue.js Best Practices
Comprehensive best practices guide for Vue.js 3 applications. Contains guidelines across multiple categories to ensure idiomatic, maintainable, and scalable Vue.js code, including Tailwind CSS integration patterns for utility-first styling and PrimeVue component library best practices.
## When to Apply
Reference these guidelines when:
- Writing new Vue components or composables
- Implementing features with Composition API
- Reviewing code for Vue.js patterns compliance
- Refactoring existing Vue.js code
- Setting up component architecture
- Working with Nuxt.js applications
- Styling Vue components with Tailwind CSS utility classes
- Creating design systems with Tailwind and Vue
- Using PrimeVue component library
- Customizing PrimeVue components with PassThrough API
## Rule Categories
| Category | Focus | Prefix |
|----------|-------|--------|
| Composition API | Proper use of Composition API patterns | `composition-` |
| Component Design | Component structure and organization | `component-` |
| Reactivity | Reactive state management patterns | `reactive-` |
| Props & Events | Component communication patterns | `props-` |
| Template Patterns | Template syntax best practices | `template-` |
| Code Organization | Project and code structure | `organization-` |
| TypeScript | Type-safe Vue.js patterns | `typescript-` |
| Error Handling | Error boundaries and handling | `error-` |
| Tailwind CSS | Utility-first styling patterns | `tailwind-` |
| PrimeVue | Component library integration patterns | `primevue-` |
## Quick Reference
### 1. Composition API Best Practices
- `composition-script-setup` - Always use `
```
### Composable Pattern
**Correct: Well-structured composable**
```typescript
// composables/useUser.ts
import { ref, computed, watch } from 'vue'
import type { Ref } from 'vue'
import type { User } from '@/types'
export function useUser(userId: Ref | string) {
// State
const user = ref(null)
const loading = ref(false)
const error = ref(null)
// Computed
const fullName = computed(() => {
if (!user.value) return ''
return `${user.value.firstName} ${user.value.lastName}`
})
// Methods
async function fetchUser(id: string) {
loading.value = true
error.value = null
try {
const response = await api.getUser(id)
user.value = response.data
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
// Auto-fetch when userId changes (if reactive)
if (isRef(userId)) {
watch(userId, (newId) => fetchUser(newId), { immediate: true })
} else {
fetchUser(userId)
}
// Return
return {
user: readonly(user),
fullName,
loading: readonly(loading),
error: readonly(error),
refresh: () => fetchUser(unref(userId))
}
}
```
### Props with Defaults
**Correct: Typed props with defaults**
```vue
```
### Event Handling
**Correct: Typed emits with payloads**
```vue
```
### v-model Implementation
**Correct: Custom v-model with defineModel (Vue 3.4+)**
```vue
```
**Correct: Custom v-model (Vue 3.3 and earlier)**
```vue
```
### Template Ref Typing
**Correct: Typed template refs**
```vue
```
### Provide/Inject with Types
**Correct: Type-safe provide/inject**
```typescript
// types/injection-keys.ts
import type { InjectionKey, Ref } from 'vue'
import type { User } from './user'
export const UserKey: InjectionKey> = Symbol('user')
// Parent component
import { provide, ref } from 'vue'
import { UserKey } from '@/types/injection-keys'
const user = ref({ id: '1', name: 'John' })
provide(UserKey, user)
// Child component
import { inject } from 'vue'
import { UserKey } from '@/types/injection-keys'
const user = inject(UserKey)
if (!user) {
throw new Error('User not provided')
}
```
### Error Boundary Component
**Correct: Error boundary with onErrorCaptured**
```vue
Something went wrong: {{ error.message }}
```
### Async Component Loading
**Correct: Async components with loading/error states**
```typescript
import { defineAsyncComponent } from 'vue'
const AsyncDashboard = defineAsyncComponent({
loader: () => import('./Dashboard.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // Show loading after 200ms
timeout: 10000 // Timeout after 10s
})
```
## Tailwind CSS Best Practices
Vue's component-based architecture pairs naturally with Tailwind's utility-first approach. Follow these patterns for maintainable, consistent styling.
### Utility-First Approach
Apply Tailwind utility classes directly in Vue templates for rapid, consistent styling:
**Correct: Utility classes in template**
```vue
{{ title }}
{{ description }}
```
### Class Ordering Convention
Maintain consistent class ordering for readability. Recommended order:
1. **Layout** - `flex`, `grid`, `block`, `hidden`
2. **Positioning** - `relative`, `absolute`, `fixed`
3. **Box Model** - `w-`, `h-`, `m-`, `p-`
4. **Typography** - `text-`, `font-`, `leading-`
5. **Visual** - `bg-`, `border-`, `rounded-`, `shadow-`
6. **Interactive** - `hover:`, `focus:`, `active:`
Use the official Prettier plugin (`prettier-plugin-tailwindcss`) to automatically sort classes.
### Responsive Design (Mobile-First)
Use Tailwind's responsive prefixes for mobile-first responsive design:
**Correct: Mobile-first responsive layout**
```vue
{{ item.title }}
```
**Breakpoint Reference:**
- `sm:` - 640px and up
- `md:` - 768px and up
- `lg:` - 1024px and up
- `xl:` - 1280px and up
- `2xl:` - 1536px and up
### State Variants
Use state variants for interactive elements:
**Correct: State variants for buttons**
```vue
```
### Dark Mode Support
Use the `dark:` prefix for dark mode styles:
**Correct: Dark mode support**
```vue
{{ title }}
{{ content }}
```
### Dynamic Classes with Computed Properties
Use computed properties for conditional class binding:
**Correct: Computed classes for variants**
```vue
```
### Class Variance Authority (CVA) Pattern
For complex component variants, use the CVA pattern with a helper library:
**Correct: CVA-style variant management**
```vue
```
### Component Extraction for Reusable Patterns
Extract repeated utility patterns into Vue components:
**Correct: Reusable card component**
```vue
```
### Tailwind Configuration with Design Tokens
Define design tokens in your Tailwind config for consistency:
**Correct: tailwind.config.js with design tokens**
```javascript
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
// Semantic color tokens
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8'
},
surface: {
light: '#ffffff',
dark: '#1f2937'
}
},
spacing: {
// Custom spacing tokens
'4.5': '1.125rem',
'18': '4.5rem'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif']
},
borderRadius: {
'4xl': '2rem'
}
}
},
plugins: []
}
```
### Tailwind CSS v4 Configuration
For Tailwind CSS v4, use the CSS-first configuration approach:
**Correct: Tailwind v4 CSS configuration**
```css
/* main.css */
@import "tailwindcss";
@theme {
/* Custom colors */
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
/* Custom spacing */
--spacing-4-5: 1.125rem;
--spacing-18: 4.5rem;
/* Custom fonts */
--font-family-sans: 'Inter', system-ui, sans-serif;
}
```
### Using `cn()` Helper for Conditional Classes
Use a class merging utility for conditional classes:
**Correct: cn() helper with clsx and tailwind-merge**
```typescript
// utils/cn.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
```
**Usage in component:**
```vue
```
## PrimeVue Best Practices
PrimeVue is a comprehensive Vue UI component library with 90+ components. Follow these patterns for effective integration and customization.
### Installation & Setup
**Correct: PrimeVue v4 setup with Vue 3**
```typescript
// main.ts
import { createApp } from 'vue'
import PrimeVue from 'primevue/config'
import Aura from '@primevue/themes/aura'
import App from './App.vue'
const app = createApp(App)
app.use(PrimeVue, {
theme: {
preset: Aura,
options: {
darkModeSelector: '.dark-mode'
}
}
})
app.mount('#app')
```
**Correct: Component registration (tree-shakeable)**
```typescript
// main.ts - Register only components you use
import Button from 'primevue/button'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
app.component('Button', Button)
app.component('DataTable', DataTable)
app.component('Column', Column)
```
### PassThrough (PT) API
The PassThrough API allows customization of internal DOM elements without modifying component source:
**Correct: Component-level PassThrough**
```vue
Panel content here
```
**Correct: Dynamic PassThrough with state**
```vue
Content changes header style when collapsed
```
### Global PassThrough Configuration
Define shared styles at the application level:
**Correct: Global PT configuration**
```typescript
// main.ts
import PrimeVue from 'primevue/config'
import Aura from '@primevue/themes/aura'
app.use(PrimeVue, {
theme: {
preset: Aura
},
pt: {
// All buttons get consistent styling
button: {
root: {
class: 'rounded-lg font-medium transition-all duration-200'
}
},
// All inputs get consistent styling
inputtext: {
root: {
class: 'rounded-lg border-2 focus:ring-2 focus:ring-primary-500'
}
},
// All panels share styling
panel: {
header: {
class: 'bg-surface-50 dark:bg-surface-900'
}
},
// Global CSS injection
global: {
css: `
.p-component {
font-family: 'Inter', sans-serif;
}
`
}
}
})
```
### usePassThrough Utility
Extend existing presets with custom modifications:
**Correct: Extending Tailwind preset**
```typescript
// presets/custom-tailwind.ts
import { usePassThrough } from 'primevue/passthrough'
import Tailwind from 'primevue/passthrough/tailwind'
export const CustomTailwind = usePassThrough(
Tailwind,
{
panel: {
header: {
class: ['bg-gradient-to-r from-primary-500 to-primary-600']
},
title: {
class: ['text-white font-bold']
}
},
button: {
root: {
class: ['shadow-lg hover:shadow-xl transition-shadow']
}
}
},
{
mergeSections: true, // Keep original sections
mergeProps: false // Replace props (don't merge arrays)
}
)
```
**Merge Strategy Reference:**
| mergeSections | mergeProps | Behavior |
|---------------|------------|----------|
| `true` | `false` | Custom value replaces original (default) |
| `true` | `true` | Custom values merge with original |
| `false` | `true` | Only custom sections included |
| `false` | `false` | Minimal - only custom sections, no merging |
### Unstyled Mode with Tailwind
Use unstyled PrimeVue components with full Tailwind control:
**Correct: Unstyled mode configuration**
```typescript
// main.ts
import PrimeVue from 'primevue/config'
app.use(PrimeVue, {
unstyled: true // Remove all default styles
})
```
**Correct: Custom styled button with unstyled mode**
```vue
```
### Wrapper Components Pattern
Create reusable wrapper components for consistent styling:
**Correct: Button wrapper component**
```vue
```
**Usage:**
```vue
Submit Form
Cancel
```
### DataTable Best Practices
**Correct: Typed DataTable with Composition API**
```vue
first = e.first"
@sort="(e) => { sortField = e.sortField; sortOrder = e.sortOrder }"
>
{{ data.status }}
```
### Form Components Pattern
**Correct: Form with validation using PrimeVue**
```vue
```
### Dialog & Overlay Patterns
**Correct: Confirmation dialog with composable**
```typescript
// composables/useConfirmDialog.ts
import { useConfirm } from 'primevue/useconfirm'
export function useConfirmDialog() {
const confirm = useConfirm()
function confirmDelete(
message: string,
onAccept: () => void,
onReject?: () => void
) {
confirm.require({
message,
header: 'Confirm Delete',
icon: 'pi pi-exclamation-triangle',
rejectClass: 'p-button-secondary p-button-outlined',
acceptClass: 'p-button-danger',
rejectLabel: 'Cancel',
acceptLabel: 'Delete',
accept: onAccept,
reject: onReject
})
}
function confirmAction(options: {
message: string
header: string
onAccept: () => void
onReject?: () => void
}) {
confirm.require({
message: options.message,
header: options.header,
icon: 'pi pi-info-circle',
rejectClass: 'p-button-secondary p-button-outlined',
acceptClass: 'p-button-primary',
accept: options.onAccept,
reject: options.onReject
})
}
return {
confirmDelete,
confirmAction
}
}
```
**Usage:**
```vue
```
### Toast Notifications
**Correct: Toast service with composable**
```typescript
// composables/useNotifications.ts
import { useToast } from 'primevue/usetoast'
export function useNotifications() {
const toast = useToast()
function success(summary: string, detail?: string) {
toast.add({
severity: 'success',
summary,
detail,
life: 3000
})
}
function error(summary: string, detail?: string) {
toast.add({
severity: 'error',
summary,
detail,
life: 5000
})
}
function warn(summary: string, detail?: string) {
toast.add({
severity: 'warn',
summary,
detail,
life: 4000
})
}
function info(summary: string, detail?: string) {
toast.add({
severity: 'info',
summary,
detail,
life: 3000
})
}
return { success, error, warn, info }
}
```
### Accessibility Best Practices
PrimeVue components are WCAG 2.0 compliant. Ensure proper usage:
**Correct: Accessible form fields**
```vue
```
### Don't Use Conflicting Utilities
Applying multiple utilities that target the same CSS property causes unpredictable results:
**Incorrect:**
```vue
Content
Content
```
**Correct:**
```vue
Content
Content
```
### Don't Ignore Accessibility
Always include proper accessibility attributes alongside visual styling:
**Incorrect:**
```vue
```
**Correct:**
```vue
```
### Don't Create Overly Long Class Strings
Break down complex class combinations into logical groups or components:
**Incorrect:**
```vue
```
**Correct: Extract to component or use computed**
```vue
```
### Don't Override PrimeVue Styles with CSS
Using CSS overrides bypasses the design system and causes maintenance issues:
**Incorrect:**
```css
/* styles.css - Avoid this approach */
.p-button {
background-color: #3b82f6 !important;
border-radius: 8px !important;
}
.p-datatable .p-datatable-thead > tr > th {
background: #f3f4f6 !important;
}
```
**Correct: Use design tokens or PassThrough**
```typescript
// main.ts - Use design tokens
app.use(PrimeVue, {
theme: {
preset: Aura,
options: {
cssLayer: {
name: 'primevue',
order: 'tailwind-base, primevue, tailwind-utilities'
}
}
},
pt: {
button: {
root: { class: 'rounded-lg' }
}
}
})
```
### Don't Import Entire PrimeVue Library
Importing everything bloats bundle size:
**Incorrect:**
```typescript
// main.ts - Don't do this
import PrimeVue from 'primevue/config'
import * as PrimeVueComponents from 'primevue' // Imports everything!
Object.entries(PrimeVueComponents).forEach(([name, component]) => {
app.component(name, component)
})
```
**Correct: Import only what you need**
```typescript
// main.ts - Tree-shakeable imports
import Button from 'primevue/button'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
app.component('Button', Button)
app.component('DataTable', DataTable)
app.component('Column', Column)
```
### Don't Mix Styled and Unstyled Inconsistently
Mixing modes creates visual inconsistency:
**Incorrect:**
```typescript
// main.ts
app.use(PrimeVue, {
unstyled: true // Global unstyled
})
// SomeComponent.vue - Using styled component anyway
// No styles applied, looks broken
```
**Correct: Choose one approach consistently**
```typescript
// Option 1: Styled mode with PT customization
app.use(PrimeVue, {
theme: { preset: Aura },
pt: { /* global customizations */ }
})
// Option 2: Unstyled mode with complete PT styling
app.use(PrimeVue, {
unstyled: true,
pt: {
button: {
root: { class: 'px-4 py-2 bg-primary-600 text-white rounded-lg' }
}
// ... complete styling for all components
}
})
```
### Don't Ignore Accessibility Attributes
PrimeVue provides accessibility out of the box, don't disable or ignore it:
**Incorrect:**
```vue
Invalid email
```
**Correct: Maintain accessibility**
```vue
Invalid email
```
### Don't Hardcode PassThrough in Every Component
Repeating PT configuration across components creates duplication:
**Incorrect:**
```vue
```
**Correct: Use global PT or wrapper components**
```typescript
// main.ts - Global configuration
app.use(PrimeVue, {
pt: {
button: {
root: { class: 'rounded-lg shadow-md' }
}
}
})
// Or use wrapper components (see Wrapper Components Pattern above)
```
## Nuxt.js Specific Guidelines
When using Nuxt.js, follow these additional patterns:
- **Auto-imports**: Leverage Nuxt's auto-imports for Vue APIs and composables
- **useFetch/useAsyncData**: Use Nuxt's data fetching composables for SSR-compatible data loading
- **definePageMeta**: Use for page-level metadata and middleware
- **Server routes**: Use `server/api/` for API endpoints
- **Runtime config**: Use `useRuntimeConfig()` for environment variables
## References
### Vue.js
- [Vue.js Documentation](https://vuejs.org)
- [Vue.js Style Guide](https://vuejs.org/style-guide/)
- [Composition API FAQ](https://vuejs.org/guide/extras/composition-api-faq.html)
- [VueUse - Collection of Vue Composition Utilities](https://vueuse.org)
- [Nuxt Documentation](https://nuxt.com)
- [Pinia Documentation](https://pinia.vuejs.org)
### Tailwind CSS
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
- [Styling with Utility Classes](https://tailwindcss.com/docs/styling-with-utility-classes)
- [Tailwind CSS v4 Release](https://tailwindcss.com/blog/tailwindcss-v4)
- [Class Variance Authority (CVA)](https://cva.style/docs)
- [tailwind-merge](https://github.com/dcastil/tailwind-merge)
- [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss)
- [Vue School - Tailwind CSS Fundamentals](https://vueschool.io/courses/tailwind-css-fundamentals)
### PrimeVue
- [PrimeVue Documentation](https://primevue.org/)
- [PrimeVue PassThrough API](https://primevue.org/passthrough/)
- [PrimeVue Theming - Styled Mode](https://primevue.org/theming/styled/)
- [PrimeVue GitHub Repository](https://github.com/primefaces/primevue)
- [PrimeVue v4 Component Changes](https://github.com/primefaces/primevue/wiki/v4-Component-Changes)
- [Volt - Tailwind CSS based PrimeVue Components](https://volt.primevue.org/)
- [Deep Dive into PrimeVue PassThrough Props](https://dev.to/cagataycivici/deep-dive-into-primevue-passthrough-props-2im8)
- [Build Your Own Vue UI Library with Unstyled PrimeVue](https://dev.to/cagataycivici/build-your-own-vue-ui-library-with-unstyled-primevue-core-and-tailwind-css-23ll)
- [PrimeIcons](https://primevue.org/icons/)