| name | vuetify |
| description | Vuetify 3 component patterns, theming, data tables, forms, grid system. Trigger: When using Vuetify components - v-btn, v-card, v-data-table, v-form, v-dialog.
|
| license | Apache-2.0 |
| metadata | {"author":"gentleman-programming","version":"1.0"} |
When to Use
- Building UI with Vuetify components
- Creating forms with validation
- Configuring themes (dark mode, custom colors)
- Setting up data tables with pagination/sorting
- Using the grid system (v-container, v-row, v-col)
- Navigation patterns (app-bar, drawer, bottom-nav)
Nuxt 4 Integration
export default defineNuxtConfig({
modules: ['vuetify-nuxt-module'],
vuetify: {
moduleOptions: {
treeshaking: true,
styles: { configFile: 'assets/settings.scss' },
},
vuetifyOptions: {
theme: {
defaultTheme: 'gentleman',
themes: {
gentleman: {
dark: true,
colors: {
primary: '#7FB4CA',
secondary: '#A3B5D6',
accent: '#E0C15A',
success: '#B7CC85',
error: '#CB7C94',
background: '#1a1b26',
surface: '#24283b',
},
},
},
},
},
},
})
Critical Patterns
Theming
import { createVuetify } from 'vuetify'
export default createVuetify({
theme: {
defaultTheme: 'dark',
themes: {
dark: {
dark: true,
colors: {
primary: '#7FB4CA',
secondary: '#A3B5D6',
accent: '#E0C15A',
error: '#CB7C94',
background: '#1a1b26',
surface: '#24283b',
},
},
light: {
dark: false,
colors: {
primary: '#1976D2',
secondary: '#424242',
},
},
},
},
})
<!-- Toggle theme -->
<script setup>
import { useTheme } from 'vuetify'
const theme = useTheme()
const toggleTheme = () => {
theme.global.name.value = theme.global.current.value.dark ? 'light' : 'dark'
}
</script>
Grid System
<template>
<v-container>
<v-row>
<v-col cols="12" md="6" lg="4">
<!-- Full width mobile, half tablet, third desktop -->
</v-col>
<v-col cols="12" md="6" lg="4">
<!-- Content -->
</v-col>
</v-row>
</v-container>
</template>
<!-- Responsive with useDisplay -->
<script setup>
import { useDisplay } from 'vuetify'
const { mobile, mdAndUp, lgAndUp, name } = useDisplay()
</script>
Breakpoints: xs (<600), sm (600-959), md (960-1279), lg (1280-1919), xl (1920-2559), xxl (2560+)
Forms & Validation
<template>
<v-form ref="form" v-model="valid" @submit.prevent="onSubmit">
<v-text-field
v-model="email"
label="Email"
:rules="emailRules"
type="email"
required
/>
<v-text-field
v-model="password"
label="Password"
:rules="passwordRules"
type="password"
required
/>
<v-select
v-model="role"
label="Role"
:items="['Admin', 'User', 'Moderator']"
:rules="[v => !!v || 'Role is required']"
/>
<v-btn type="submit" :disabled="!valid" color="primary">Submit</v-btn>
</v-form>
</template>
<script setup lang="ts">
const form = ref()
const valid = ref(false)
const email = ref('')
const password = ref('')
const role = ref('')
// Validation rules — array of functions returning true or error string
const emailRules = [
(v: string) => !!v || 'Email is required',
(v: string) => /.+@.+\..+/.test(v) || 'Email must be valid',
]
const passwordRules = [
(v: string) => !!v || 'Password is required',
(v: string) => v.length >= 8 || 'Min 8 characters',
]
async function onSubmit() {
const { valid } = await form.value.validate()
if (!valid) return
// Submit logic
}
// Reset form
function resetForm() {
form.value.reset() // Clear values
form.value.resetValidation() // Clear errors only
}
</script>
Reusable Validation Rules
export const rules = {
required: (v: any) => !!v || 'Field is required',
email: (v: string) => /.+@.+\..+/.test(v) || 'Invalid email',
minLength: (min: number) => (v: string) => v.length >= min || `Min ${min} characters`,
maxLength: (max: number) => (v: string) => v.length <= max || `Max ${max} characters`,
numeric: (v: string) => /^\d+$/.test(v) || 'Must be numeric',
}
Data Tables
Client-Side
<template>
<v-data-table
:headers="headers"
:items="users"
:search="search"
:items-per-page="10"
:sort-by="[{ key: 'name', order: 'asc' }]"
>
<template #top>
<v-text-field v-model="search" label="Search" prepend-inner-icon="mdi-magnify" />
</template>
<template #item.actions="{ item }">
<v-btn icon="mdi-pencil" size="small" @click="editUser(item)" />
<v-btn icon="mdi-delete" size="small" color="error" @click="deleteUser(item)" />
</template>
</v-data-table>
</template>
<script setup lang="ts">
const search = ref('')
const headers = [
{ title: 'Name', key: 'name', sortable: true },
{ title: 'Email', key: 'email', sortable: true },
{ title: 'Role', key: 'role', sortable: true },
{ title: 'Actions', key: 'actions', sortable: false },
]
</script>
Server-Side Pagination
<template>
<v-data-table-server
:headers="headers"
:items="items"
:items-length="totalItems"
:loading="loading"
:items-per-page="itemsPerPage"
@update:options="loadItems"
>
</v-data-table-server>
</template>
<script setup lang="ts">
const items = ref([])
const totalItems = ref(0)
const loading = ref(false)
const itemsPerPage = ref(10)
async function loadItems({ page, itemsPerPage, sortBy, search }: any) {
loading.value = true
const { data, total } = await $fetch('/api/users', {
query: {
page,
limit: itemsPerPage,
sortBy: sortBy[0]?.key,
sortOrder: sortBy[0]?.order,
search,
},
})
items.value = data
totalItems.value = total
loading.value = false
}
</script>
Navigation Layout
<!-- layouts/default.vue -->
<template>
<v-app>
<v-app-bar color="primary" density="comfortable">
<v-app-bar-nav-icon @click="drawer = !drawer" />
<v-app-bar-title>My App</v-app-bar-title>
<v-spacer />
<v-btn icon="mdi-theme-light-dark" @click="toggleTheme" />
</v-app-bar>
<v-navigation-drawer v-model="drawer" :rail="rail" permanent @click="rail = false">
<v-list nav>
<v-list-item
v-for="item in navItems"
:key="item.to"
:prepend-icon="item.icon"
:title="item.title"
:to="item.to"
/>
</v-list>
</v-navigation-drawer>
<v-main>
<v-container>
<slot />
</v-container>
</v-main>
</v-app>
</template>
<script setup>
const drawer = ref(true)
const rail = ref(false)
const navItems = [
{ title: 'Dashboard', icon: 'mdi-view-dashboard', to: '/' },
{ title: 'Users', icon: 'mdi-account-group', to: '/users' },
{ title: 'Settings', icon: 'mdi-cog', to: '/settings' },
]
</script>
Dialogs & Overlays
<!-- Confirmation Dialog Pattern -->
<template>
<v-dialog v-model="dialog" max-width="500" persistent>
<v-card>
<v-card-title>{{ title }}</v-card-title>
<v-card-text>{{ message }}</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="dialog = false">Cancel</v-btn>
<v-btn color="error" variant="flat" :loading="loading" @click="confirm">
Confirm
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<!-- Menu -->
<v-menu>
<template #activator="{ props }">
<v-btn v-bind="props" icon="mdi-dots-vertical" />
</template>
<v-list>
<v-list-item v-for="action in actions" :key="action.title" @click="action.handler">
<v-list-item-title>{{ action.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<!-- Tooltip -->
<v-tooltip text="Delete this item" location="top">
<template #activator="{ props }">
<v-btn v-bind="props" icon="mdi-delete" color="error" />
</template>
</v-tooltip>
Composables
import { useDisplay, useTheme, useLayout } from 'vuetify'
const { mobile, mdAndUp, lgAndUp, name, width, height } = useDisplay()
const theme = useTheme()
theme.global.name.value = 'dark'
const isDark = computed(() => theme.global.current.value.dark)
const { mainRect, mainStyles } = useLayout()
Decision Tree
Need a data grid? → v-data-table (client) or v-data-table-server (API)
Need form validation? → v-form with :rules array
Need responsive layout? → v-container + v-row + v-col
Need sidebar navigation? → v-navigation-drawer (rail mode for collapsed)
Need confirmation action? → v-dialog with persistent prop
Need dropdown actions? → v-menu with activator slot
Need feedback/loading? → v-snackbar, v-progress-linear, v-skeleton-loader
Need theme toggle? → useTheme() composable
Need responsive logic in JS? → useDisplay() composable
Component Prop Conventions
<!-- Button variants -->
<v-btn variant="flat">Primary</v-btn> <!-- Filled -->
<v-btn variant="outlined">Outlined</v-btn>
<v-btn variant="text">Text</v-btn>
<v-btn variant="tonal">Tonal</v-btn>
<!-- Density -->
<v-text-field density="compact" /> <!-- compact, comfortable, default -->
<!-- Loading states -->
<v-btn :loading="saving" @click="save">Save</v-btn>
<v-skeleton-loader type="card" :loading="loading" />
<!-- Snackbar (feedback) -->
<v-snackbar v-model="snackbar" :color="snackColor" :timeout="3000">
{{ snackMessage }}
</v-snackbar>