--- 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 ``` ### 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 ``` ### 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 ``` **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 ``` ### 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 ``` **Correct: Dynamic PassThrough with state** ```vue ``` ### 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 ``` ### DataTable Best Practices **Correct: Typed DataTable with Composition API** ```vue ``` ### 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 ``` ### Lazy Loading Components **Correct: Async component loading for large PrimeVue components** ```typescript // components/lazy/index.ts import { defineAsyncComponent } from 'vue' export const LazyDataTable = defineAsyncComponent({ loader: () => import('primevue/datatable'), loadingComponent: () => import('@/components/ui/TableSkeleton.vue'), delay: 200 }) export const LazyEditor = defineAsyncComponent({ loader: () => import('primevue/editor'), loadingComponent: () => import('@/components/ui/EditorSkeleton.vue'), delay: 200 }) export const LazyChart = defineAsyncComponent({ loader: () => import('primevue/chart'), loadingComponent: () => import('@/components/ui/ChartSkeleton.vue'), delay: 200 }) ``` ## Anti-Patterns to Avoid ### Don't Mutate Props **Incorrect:** ```vue ``` **Correct:** ```vue ``` ### Don't Use v-if with v-for **Incorrect:** ```vue ``` **Correct:** ```vue ``` ### Don't Store Derived State **Incorrect:** ```vue ``` **Correct:** ```vue ``` ### Don't Destructure Reactive Objects **Incorrect:** ```vue ``` **Correct:** ```vue ``` ### Don't Concatenate Tailwind Class Names Dynamic class concatenation breaks Tailwind's compiler and classes get purged in production: **Incorrect:** ```vue ``` **Correct:** ```vue ``` ### Don't Overuse @apply Excessive `@apply` usage defeats the purpose of utility-first CSS: **Incorrect:** ```css /* styles.css */ .card { @apply mx-auto max-w-md rounded-xl bg-white p-6 shadow-lg; } .card-title { @apply text-xl font-semibold text-gray-900; } .card-description { @apply mt-2 text-gray-600; } .card-button { @apply mt-4 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700; } ``` **Correct: Use Vue components instead** ```vue ``` ### Don't Use Conflicting Utilities Applying multiple utilities that target the same CSS property causes unpredictable results: **Incorrect:** ```vue ``` **Correct:** ```vue ``` ### 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