vue-best-practices
π―Skillfrom dedalus-erp-pas/foundation-skills
Provides comprehensive Vue.js 3 best practices guidelines for writing idiomatic, maintainable code using Composition API, Tailwind CSS, and PrimeVue.
Part of
dedalus-erp-pas/foundation-skills(21 items)
Installation
npx add-skill Dedalus-ERP-PAS/foundation-skills -g -ySkill Details
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.
Overview
# 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 usefor single-file componentscomposition-ref-vs-reactive- Useref()for primitives,reactive()for objectscomposition-computed-derived- Usecomputed()for all derived statecomposition-watch-side-effects- Usewatch()/watchEffect()only for side effectscomposition-composables- Extract reusable logic into composablescomposition-lifecycle-order- Place lifecycle hooks after reactive state declarationscomposition-avoid-this- Never usethisin Composition API
2. Component Design
component-single-responsibility- One component, one purposecomponent-naming-convention- Use PascalCase for components, kebab-case in templatescomponent-small-focused- Keep components under 200 linescomponent-presentational-container- Separate logic from presentation when beneficialcomponent-slots-flexibility- Use slots for flexible component compositioncomponent-expose-minimal- Only expose what's necessary viadefineExpose()
3. Reactivity Patterns
reactive-const-refs- Always declare refs withconstreactive-unwrap-template- Let Vue unwrap refs in templates (no.value)reactive-shallow-large-data- UseshallowRef()/shallowReactive()for large non-reactive datareactive-readonly-props- Usereadonly()to prevent mutationsreactive-toRefs-destructure- UsetoRefs()when destructuring reactive objectsreactive-avoid-mutation- Prefer immutable updates for complex state
4. Props & Events
props-define-types- Always define prop types withdefineProps() props-required-explicit- Be explicit about required vs optional propsprops-default-values- Provide sensible defaults withwithDefaults()props-immutable- Never mutate props directlyprops-validation- Use validator functions for complex prop validationevents-define-emits- Always define emits withdefineEmits() events-naming- Use kebab-case for event names in templatesevents-payload-objects- Pass objects for events with multiple values
5. Template Patterns
template-v-if-v-show- Usev-iffor conditional rendering,v-showfor togglingtemplate-v-for-key- Always use unique, stable:keywithv-fortemplate-v-if-v-for- Never usev-ifandv-foron the same elementtemplate-computed-expressions- Move complex expressions to computed propertiestemplate-event-modifiers- Use event modifiers (.prevent,.stop) appropriatelytemplate-v-bind-shorthand- Use shorthand syntax (:forv-bind,@forv-on)template-v-model-modifiers- Use v-model modifiers (.trim,.number,.lazy)
6. Code Organization
organization-feature-folders- Organize by feature, not by typeorganization-composables-folder- Keep composables in dedicatedcomposables/folderorganization-barrel-exports- Use index files for clean importsorganization-consistent-naming- Follow consistent naming conventionsorganization-colocation- Colocate related files (component, tests, styles)
7. TypeScript Integration
typescript-generic-components- Use generics for reusable typed componentstypescript-prop-types- Use TypeScript interfaces for prop definitionstypescript-emit-types- Type emit payloads explicitlytypescript-ref-typing- Specify types for refs when not inferredtypescript-template-refs- Type template refs withref| null>(null)
8. Error Handling
error-boundaries- UseonErrorCaptured()for component error boundarieserror-async-handling- Handle errors in async operations explicitlyerror-provide-fallbacks- Provide fallback UI for error stateserror-logging- Log errors appropriately for debugging
9. Tailwind CSS
tailwind-utility-first- Apply utility classes directly in templates, avoid custom CSStailwind-class-order- Use consistent class ordering (layout β spacing β typography β visual)tailwind-responsive-mobile-first- Use mobile-first responsive design (sm:,md:,lg:)tailwind-component-extraction- Extract repeated utility patterns into Vue componentstailwind-dynamic-classes- Use computed properties or helper functions for dynamic classestailwind-complete-class-strings- Always use complete class strings, never concatenatetailwind-state-variants- Use state variants (hover:,focus:,active:) for interactionstailwind-dark-mode- Usedark:prefix for dark mode supporttailwind-design-tokens- Configure design tokens in Tailwind config for consistencytailwind-avoid-apply-overuse- Limit@applyusage; prefer Vue components for abstraction
10. PrimeVue
primevue-design-tokens- Use design tokens over CSS overrides for themingprimevue-passthrough-api- Use PassThrough (pt) API for component customizationprimevue-wrapper-components- Wrap PrimeVue components for consistent styling across appsprimevue-unstyled-mode- Use unstyled mode with Tailwind for full styling controlprimevue-global-pt-config- Define shared PassThrough properties at app levelprimevue-merge-strategies- Choose appropriate merge strategies for PT customizationprimevue-use-passthrough-utility- UseusePassThroughfor extending presetsprimevue-typed-components- Leverage PrimeVue's TypeScript support for type safetyprimevue-accessibility- Maintain WCAG compliance with proper aria attributesprimevue-lazy-loading- Use async components for large PrimeVue imports
Key Principles
Composition API Best Practices
The Composition API is the recommended approach for Vue.js 3. Follow these patterns:
- Always use
: More concise, better TypeScript inference, and improved performance - Organize code by logical concern: Group related state, computed properties, and functions together
- Extract reusable logic to composables: Follow the
useprefix convention (e.g.,useAuth,useFetch) - Keep setup code readable: Order: props/emits, reactive state, computed, watchers, methods, lifecycle hooks
Component Design Principles
Well-designed components are the foundation of maintainable Vue applications:
- Single Responsibility: Each component should do one thing well
- Props Down, Events Up: Follow unidirectional data flow
- Prefer Composition over Inheritance: Use composables and slots for code reuse
- Keep Components Small: If a component exceeds 200 lines, consider splitting it
Reactivity Guidelines
Understanding Vue's reactivity system is crucial:
- ref vs reactive: Use
ref()for primitives and values you'll reassign; usereactive()for objects you'll mutate - Computed for derived state: Never store derived state in refs; use
computed()instead - Watch for side effects: Only use
watch()for side effects like API calls or localStorage - Be mindful of reactivity loss: Don't destructure reactive objects without
toRefs()
Props & Events Patterns
Proper component communication ensures maintainable code:
- Type your props: Use TypeScript interfaces with
defineProps() - Validate complex props: Use validator functions for business logic validation
- Emit typed events: Use
defineEmitsfor type-safe event handling() - Use v-model for two-way binding: Implement
modelValueprop andupdate:modelValueemit
Common Patterns
Script Setup Structure
Recommended structure for :
```vue
// 1. Imports
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import type { User } from '@/types'
// 2. Props and Emits
const props = defineProps<{
userId: string
initialData?: User
}>()
const emit = defineEmits<{
submit: [user: User]
cancel: []
}>()
// 3. Composables
const router = useRouter()
const { user, loading, error } = useUser(props.userId)
// 4. Reactive State
const formData = ref({ name: '', email: '' })
const isEditing = ref(false)
// 5. Computed Properties
const isValid = computed(() => {
return formData.value.name.length > 0 && formData.value.email.includes('@')
})
// 6. Watchers (for side effects only)
watch(() => props.userId, (newId) => {
fetchUserData(newId)
})
// 7. Methods
function handleSubmit() {
if (isValid.value) {
emit('submit', formData.value)
}
}
// 8. Lifecycle Hooks
onMounted(() => {
if (props.initialData) {
formData.value = { ...props.initialData }
}
})
```
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
// State
const user = ref
const loading = ref(false)
const error = ref
// 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
interface Props {
title: string
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
items?: string[]
}
const props = withDefaults(defineProps
size: 'md',
disabled: false,
items: () => [] // Use factory function for arrays/objects
})
```
Event Handling
Correct: Typed emits with payloads
```vue
interface FormData {
name: string
email: string
}
const emit = defineEmits<{
submit: [data: FormData]
cancel: []
'update:modelValue': [value: string]
}>()
function handleSubmit(data: FormData) {
emit('submit', data)
}
```
v-model Implementation
Correct: Custom v-model with defineModel (Vue 3.4+)
```vue
const model = defineModel
// Or with default
const modelWithDefault = defineModel
```
Correct: Custom v-model (Vue 3.3 and earlier)
```vue
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const value = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
```
Template Ref Typing
Correct: Typed template refs
```vue
import { ref, onMounted } from 'vue'
import MyComponent from './MyComponent.vue'
// DOM element ref
const inputRef = ref
// Component ref
const componentRef = ref
onMounted(() => {
inputRef.value?.focus()
componentRef.value?.someExposedMethod()
})
```
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
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
import { ref, onErrorCaptured } from 'vue'
const error = ref
onErrorCaptured((err) => {
error.value = err
// Return false to stop error propagation
return false
})
function reset() {
error.value = null
}
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
{{ description }} {{ buttonText }} {{ title }}
```
Class Ordering Convention
Maintain consistent class ordering for readability. Recommended order:
- Layout -
flex,grid,block,hidden - Positioning -
relative,absolute,fixed - Box Model -
w-,h-,m-,p- - Typography -
text-,font-,leading- - Visual -
bg-,border-,rounded-,shadow- - 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
v-for="item in items" :key="item.id" class="p-4 text-sm sm:p-6 sm:text-base lg:text-lg" > {{ item.title }}
```
Breakpoint Reference:
sm:- 640px and upmd:- 768px and uplg:- 1024px and upxl:- 1280px and up2xl:- 1536px and up
State Variants
Use state variants for interactive elements:
Correct: State variants for buttons
```vue
class="rounded-lg bg-indigo-600 px-4 py-2 font-medium text-white
transition-colors duration-150
hover:bg-indigo-700
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2
active:bg-indigo-800
disabled:cursor-not-allowed disabled:opacity-50"
:disabled="isLoading"
>
{{ isLoading ? 'Loading...' : 'Submit' }}
```
Dark Mode Support
Use the dark: prefix for dark mode styles:
Correct: Dark mode support
```vue
{{ content }} {{ title }}
```
Dynamic Classes with Computed Properties
Use computed properties for conditional class binding:
Correct: Computed classes for variants
```vue
import { computed } from 'vue'
type ButtonVariant = 'primary' | 'secondary' | 'danger'
type ButtonSize = 'sm' | 'md' | 'lg'
const props = withDefaults(defineProps<{
variant?: ButtonVariant
size?: ButtonSize
}>(), {
variant: 'primary',
size: 'md'
})
const variantClasses = computed(() => {
const variants: Record
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500'
}
return variants[props.variant]
})
const sizeClasses = computed(() => {
const sizes: Record
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
}
return sizes[props.size]
})
const buttonClasses = computed(() => [
'inline-flex items-center justify-center rounded-md font-medium',
'transition-colors duration-150',
'focus:outline-none focus:ring-2 focus:ring-offset-2',
variantClasses.value,
sizeClasses.value
])
```
Class Variance Authority (CVA) Pattern
For complex component variants, use the CVA pattern with a helper library:
Correct: CVA-style variant management
```vue
import { computed } from 'vue'
import { cva, type VariantProps } from 'class-variance-authority'
const button = cva(
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
{
variants: {
intent: {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500'
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
}
},
defaultVariants: {
intent: 'primary',
size: 'md'
}
}
)
type ButtonProps = VariantProps
const props = defineProps<{
intent?: ButtonProps['intent']
size?: ButtonProps['size']
}>()
const classes = computed(() => button({ intent: props.intent, size: props.size }))
```
Component Extraction for Reusable Patterns
Extract repeated utility patterns into Vue components:
Correct: Reusable card component
```vue
withDefaults(defineProps<{
padding?: 'none' | 'sm' | 'md' | 'lg'
shadow?: 'none' | 'sm' | 'md' | 'lg'
}>(), {
padding: 'md',
shadow: 'md'
})
class="rounded-xl bg-white dark:bg-gray-800" :class="[ { 'p-0': padding === 'none', 'p-4': padding === 'sm', 'p-6': padding === 'md', 'p-8': padding === 'lg' }, { 'shadow-none': shadow === 'none', 'shadow-sm': shadow === 'sm', 'shadow-md': shadow === 'md', 'shadow-lg': shadow === 'lg' } ]" >
```
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
import { cn } from '@/utils/cn'
const props = defineProps<{
class?: string
isActive?: boolean
}>()
:class="cn( 'rounded-lg border p-4 transition-colors', isActive ? 'border-blue-500 bg-blue-50' : 'border-gray-200 bg-white', props.class )" >
```
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
import Panel from 'primevue/panel'
header="User Profile"
toggleable
:pt="{
header: {
class: 'bg-primary-100 dark:bg-primary-900'
},
content: {
class: 'p-6'
},
title: {
class: 'text-xl font-semibold'
},
toggler: {
class: 'hover:bg-primary-200 dark:hover:bg-primary-800 rounded-full'
}
}"
>
Panel content here
```
Correct: Dynamic PassThrough with state
```vue
import Panel from 'primevue/panel'
header="Collapsible Panel"
toggleable
:pt="{
header: (options) => ({
class: [
'transition-colors duration-200',
{
'bg-primary-500 text-white': options.state.d_collapsed,
'bg-surface-100 dark:bg-surface-800': !options.state.d_collapsed
}
]
})
}"
>
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
import Button from 'primevue/button'
label="Submit"
:pt="{
root: {
class: [
'inline-flex items-center justify-center',
'px-4 py-2 rounded-lg font-medium',
'bg-primary-600 text-white',
'hover:bg-primary-700 active:bg-primary-800',
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
'transition-colors duration-150',
'disabled:opacity-50 disabled:cursor-not-allowed'
]
},
label: {
class: 'font-medium'
},
icon: {
class: 'mr-2'
}
}"
:ptOptions="{ mergeSections: false, mergeProps: false }"
/>
```
Wrapper Components Pattern
Create reusable wrapper components for consistent styling:
Correct: Button wrapper component
```vue
import Button from 'primevue/button'
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost'
type ButtonSize = 'sm' | 'md' | 'lg'
const props = withDefaults(defineProps<{
variant?: ButtonVariant
size?: ButtonSize
loading?: boolean
}>(), {
variant: 'primary',
size: 'md',
loading: false
})
const variantClasses: Record
primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500',
secondary: 'bg-surface-200 text-surface-900 hover:bg-surface-300 focus:ring-surface-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
ghost: 'bg-transparent text-primary-600 hover:bg-primary-50 focus:ring-primary-500'
}
const sizeClasses: Record
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
}
v-bind="$attrs"
:loading="loading"
:pt="{
root: {
class: [
'inline-flex items-center justify-center rounded-lg font-medium',
'transition-all duration-200',
'focus:outline-none focus:ring-2 focus:ring-offset-2',
'disabled:opacity-50 disabled:cursor-not-allowed',
variantClasses[variant],
sizeClasses[size]
]
}
}"
:ptOptions="{ mergeSections: false, mergeProps: false }"
>
export default {
inheritAttrs: false
}
```
Usage:
```vue
Submit Form
Cancel
```
DataTable Best Practices
Correct: Typed DataTable with Composition API
```vue
import { ref, onMounted } from 'vue'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
interface User {
id: number
name: string
email: string
role: string
status: 'active' | 'inactive'
}
const users = ref
const loading = ref(true)
const selectedUsers = ref
// Pagination
const first = ref(0)
const rows = ref(10)
// Sorting
const sortField = ref
const sortOrder = ref<1 | -1>(1)
onMounted(async () => {
loading.value = true
try {
users.value = await fetchUsers()
} finally {
loading.value = false
}
})
v-model:selection="selectedUsers"
:value="users"
:loading="loading"
:paginator="true"
:rows="rows"
:first="first"
:sortField="sortField"
:sortOrder="sortOrder"
dataKey="id"
stripedRows
removableSort
@page="(e) => first = e.first"
@sort="(e) => { sortField = e.sortField; sortOrder = e.sortOrder }"
>
:class="[
'px-2 py-1 rounded-full text-xs font-medium',
data.status === 'active'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
]"
>
{{ data.status }}
```
Form Components Pattern
Correct: Form with validation using PrimeVue
```vue
import { ref, computed } from 'vue'
import InputText from 'primevue/inputtext'
import Password from 'primevue/password'
import Dropdown from 'primevue/dropdown'
import Button from 'primevue/button'
import Message from 'primevue/message'
interface FormData {
email: string
password: string
role: string | null
}
const formData = ref
email: '',
password: '',
role: null
})
const errors = ref
const submitted = ref(false)
const roles = [
{ label: 'Admin', value: 'admin' },
{ label: 'User', value: 'user' },
{ label: 'Guest', value: 'guest' }
]
const isValid = computed(() => {
return Object.keys(errors.value).length === 0
})
function validate(): boolean {
errors.value = {}
if (!formData.value.email) {
errors.value.email = 'Email is required'
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.value.email)) {
errors.value.email = 'Invalid email format'
}
if (!formData.value.password) {
errors.value.password = 'Password is required'
} else if (formData.value.password.length < 8) {
errors.value.password = 'Password must be at least 8 characters'
}
if (!formData.value.role) {
errors.value.role = 'Role is required'
}
return Object.keys(errors.value).length === 0
}
function handleSubmit() {
submitted.value = true
if (validate()) {
// Submit form
console.log('Form submitted:', formData.value)
}
}
id="email" v-model="formData.email" :class="{ 'p-invalid': errors.email }" aria-describedby="email-error" /> {{ errors.email }}
id="password" v-model="formData.password" :class="{ 'p-invalid': errors.password }" toggleMask :feedback="false" aria-describedby="password-error" /> {{ errors.password }}
id="role" v-model="formData.role" :options="roles" optionLabel="label" optionValue="value" placeholder="Select a role" :class="{ 'p-invalid': errors.role }" aria-describedby="role-error" /> {{ errors.role }}
```
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
import { useConfirmDialog } from '@/composables/useConfirmDialog'
import ConfirmDialog from 'primevue/confirmdialog'
const { confirmDelete } = useConfirmDialog()
function handleDelete(item: Item) {
confirmDelete(
Are you sure you want to delete "${item.name}"?,
() => deleteItem(item.id)
)
}
```
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
{{ label }} * :id="id" v-model="modelValue" :aria-required="required" :aria-invalid="!!error" :aria-describedby="error ? /> v-if="error" :id=" class="text-red-500" role="alert" > {{ error }} ${id}-error : undefined"${id}-error"
```
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
const props = defineProps(['items'])
function addItem(item) {
props.items.push(item) // Never mutate props!
}
```
Correct:
```vue
const props = defineProps(['items'])
const emit = defineEmits(['update:items'])
function addItem(item) {
emit('update:items', [...props.items, item])
}
```
Don't Use v-if with v-for
Incorrect:
```vue
{{ item.name }}
```
Correct:
```vue
const activeItems = computed(() => items.value.filter(item => item.isActive))
{{ item.name }}
```
Don't Store Derived State
Incorrect:
```vue
const items = ref([])
const itemCount = ref(0) // Derived state stored separately
watch(items, () => {
itemCount.value = items.value.length // Manually syncing
})
```
Correct:
```vue
const items = ref([])
const itemCount = computed(() => items.value.length) // Computed property
```
Don't Destructure Reactive Objects
Incorrect:
```vue
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state // Loses reactivity!
```
Correct:
```vue
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state) // Preserves reactivity
```
Don't Concatenate Tailwind Class Names
Dynamic class concatenation breaks Tailwind's compiler and classes get purged in production:
Incorrect:
```vue
const color = ref('blue')
Content
```
Correct:
```vue
const color = ref<'blue' | 'green' | 'red'>('blue')
const colorClasses = computed(() => {
const colors = {
blue: 'bg-blue-500 text-blue-900',
green: 'bg-green-500 text-green-900',
red: 'bg-red-500 text-red-900'
}
return colors[color.value]
})
Content
```
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
class="rounded bg-blue-600 p-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
aria-label="Close dialog"
>
```
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
const containerClasses = [
// Layout
'mx-auto max-w-4xl flex flex-col sm:flex-row',
'items-center justify-between',
'gap-4 sm:gap-6 lg:gap-8',
// Spacing
'mt-8 p-6 md:p-8',
// Visual
'rounded-xl border bg-white shadow-lg',
'border-gray-200 dark:border-gray-700 dark:bg-gray-800',
// Interactive
'transition-all duration-300',
'hover:border-blue-500 hover:shadow-xl'
]
```
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
icon="pi pi-trash"
aria-label="Delete item"
@click="deleteItem"
/>
id="email" v-model="email" :class="{ 'p-invalid': hasError }" :aria-invalid="hasError" aria-describedby="email-error" /> 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/cour
More from this repository10
Guides developers in writing high-performance, modern React and Next.js applications by providing best practices for component architecture, performance, and UI design.
gitlab code review skill from dedalus-erp-pas/foundation-skills
postgres skill from dedalus-erp-pas/foundation-skills
article-extractor skill from dedalus-erp-pas/foundation-skills
Crafts distinctive, production-grade frontend interfaces with exceptional design quality, generating creative and polished web components and UI layouts.
Generates customized design system rules to automate Figma-to-code workflows, ensuring consistent UI/UX conventions and patterns across design and development teams.
Generates professional, user-friendly changelogs by automatically transforming git commits into clear, categorized release notes.
Performs comprehensive web UI review by analyzing code, accessibility, and design against industry guidelines with auto-fix capabilities.
Automates web testing and browser interactions using Playwright, enabling comprehensive website validation, form testing, and dynamic web application debugging.
Builds high-quality MCP servers that enable LLMs to interact with external services through well-designed, discoverable, and context-aware tools.