Vuetify Conventions
SkillDev toolsEsposter Vuetify 4 conventions — StyledButton for primary actions, the isIconButton shape switch, :to and type never inside :button-props, v-prefixed auto-imported composables (useVDisplay/useVTheme), global defaults never repeated and why a state-conditional style cannot be one, drawer elevation, tooltips on icon-only buttons, StyledTooltipIconButton/StyledTooltipMenuIconButton over a hand-rolled activator chain, plain-variant buttons inside input slots, typed SelectItemCategoryDefinition items (clearable banned, no item-title/item-value), enum-value-as-display-title, form validity naming and useVRules, the mount gate a dialog born open owes Vuetify's block scroll strategy, no SASS variables in component styles, plus deep dives on button backgrounds, router-driven highlighting of linked buttons and tabs, StyledList and StyledAvatar, form dialogs and custom validation rules, constructing items arrays from enums and maps, the CSS custom property registry, scrollspy sub-nav, and mergeProps activator stacks. Apply when writing or reviewing Vuetify components, dialogs, selects, forms, or lists.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Vuetify Conventions skill
What this skill tells your AI
The instructions your AI receives, as published by esposter/esposter in .agents/skills/vuetify/SKILL.md and read by ahel’s review.
Primary Buttons
Use StyledButton for every confirm / complete / primary call-to-action (create, save, accept, publish, request, start). Never a raw color="primary" v-btn — colourless buttons are transparent by default (references/button-backgrounds.md), so a primary-coloured fill reads badly on the app's transparent / v-main base; StyledButton renders the midnight-bloom gradient + white text instead. It paints with background-image, so it is immune to every background rule and inherited variant.
- Pass Vuetify props through
:button-props="{ ... }"(camelCase —{ prependIcon: 'mdi-plus', disabled: !isValid, loading: isSubmitting }). :to,typeand native listeners go directly on the wrapper, never inside:button-props— they fall through to the rootv-btn(typeis a native attribute, not a typedVBtnprop, sobuttonPropsfails typecheck). Link choice, the raw-<a>ban andRoutePathtargets belong to the routing skill.- Destructive confirms stay a
color="error"v-btn— error red is visible on the transparent base, andStyledButtonis for positive/primary actions only. StyledTooltipIconButtonpasses:iconby default, and Vuetify'siconprop switches the button to the icon-button variant: circular, equal width/height, no min-width. Converting a<v-tooltip>+ rectangular<v-btn>to it silently turns the button into a circle — pass:is-icon-button="false"to keep the regular button shape with the icon as a child.rounded/tilecannot restore it; they only change corners, not the forced square dimensions.
Button Backgrounds — references/button-backgrounds.md
Colourless flat buttons are transparent by app CSS, and a parent container (v-card-actions, v-toolbar, v-btn-group, …) can override the variant to "text" so a color tints the text rather than the background. Read the page before adding a fill, and never fight the container with variant="elevated" — a filled action inside one is StyledButton, an icon action is StyledTooltipIconButton, a destructive confirm is a color="error" v-btn.
Auto-Imported Composables — v Prefix
Vuetify composables are auto-imported with a v prefix and are globally available — never import from "vuetify" directly: useVDisplay(), useVTheme(), useVLocale(), useVDate(), useVGoTo(), useVRules().
Global Defaults (vuetify.config.ts)
A prop vuetify.config.ts declares must never be repeated on an instance. Read the defaults object there for the current set rather than a copy of it here — it is one screen long. Two entries recur: variant="outlined" on every text input, and a hideDetails on every input.
hideDetails: "auto" is the one worth understanding, because the value a caller reaches for instead is worse than redundant. "auto" renders the details row exactly when there is a message to put in it; bare hide-details means true, which hides the row unconditionally and so swallows the validation error a field with rules exists to report. Both the static and the bound form are eslint errors (vue/no-restricted-static-attribute and vue/no-restricted-syntax in packages/configuration/eslint/overrides/vueRules.js) — a binding computes per render what "auto" already answers per render. A layout that genuinely cannot afford the row carries a disable comment stating why; no field in the app does today, and no input is pinned to true either — true reserves exactly as little space as "auto" while also swallowing the message, so it is never the better value, not even on a switch.
A default is a constant, so anything conditional on component state cannot be one. A prop set here applies to every instance in every state, and several of the states worth styling are ones the component turns on for itself — temporary at the mobile breakpoint, --active while a drawer is on-canvas. Style those as a rule in globals.scss keyed on Vuetify's own state classes, inside the vuetify-overrides layer, using the framework's mixins so the values stay Vuetify's (@use "vuetify/tools" as vuetify). This is the one place a Vuetify SASS API is allowed — component <style> blocks still may not (below).
Every drawer goes through StyledNavigationDrawer, never v-navigation-drawer directly. A permanent drawer still honours model-value, and Vuetify only forces one open when permanent changes to true — the initial pass takes that branch for a null model alone. So a drawer bound to a ref that starts closed renders inert for the whole session, and any handler closing the mobile overlay closes the desktop rail with it. The wrapper is the single place that resolves it: while it is permanent it is open, and the model is the open state of the overlay it becomes when it is not. Bound state that is conditional on a prop cannot live in vuetify.config.ts — a default is a constant, so this is a component or nothing.
v-navigation-drawer is flat; a drawer that floats over the content is not. Vuetify shadows a drawer only while it is temporary and open, where a scrim is already holding it off the page, and that is the whole of it — there is no app-wide elevation override. A permanent or persistent drawer reserves its own column in the layout, so the content sits beside it rather than under it and a border is separation enough. StyledNavigationOverlay is the other case: an absolutely-positioned sheet over the content with no scrim, where the shadow is the only thing distinguishing the two, so it states elevation="4" itself. Elevation follows whether the surface overlaps what is behind it, not whether it is called a drawer.
Button Conventions
- Every icon-only
v-btnmust have av-tooltip— wrap withv-tooltip+ descriptivetextso the action is discoverable. A button with visible label text is self-describing and needs none. #activatorslot always first inv-tooltip(andv-menu).- Icon choice for create actions — use the semantically specific MDI icon when available (
mdi-table-row-plus-after,mdi-table-column-plus-after); fall back tomdi-plusfor generic create. - Inside a
v-text-fieldslot (#append-inneretc.) usevariant="plain"and omitcolor— avariant="flat" color="primary"button paints a filled block inside the input, where plain stays transparent and inherits the surrounding text colour.
Linked Buttons and Tabs Are Highlighted by the Router — references/router-driven-highlighting.md
Once a v-btn/v-tab carries to, Vuetify derives its highlight from the router link and ignores the group's model-value. Read the page before binding to, or when the wrong tab is lit: it owns the catch-all exact rule, the tab standing for a group of pages, and the explicit :active escape hatch.
Nested Activators — the Primitive First, mergeProps Only Beyond It
The two common stacks already have a component; reach for it before writing an activator chain at all.
| Stack | Use |
|---|---|
v-tooltip + v-btn | StyledTooltipIconButton |
v-menu + v-tooltip + v-btn | StyledTooltipMenuIconButton |
Hand-rolling either is the single most repeated finding in this area — the chain looks like plumbing rather than a component, so it gets rewritten instead of imported. Anything the two primitives do not cover (a v-dialog or v-hover in the stack, a non-icon activator, three-way nesting) binds one mergeProps(...) and never two := binds — references/nested-activators.md.
Selects and List Items
- Type items as
SelectItemCategoryDefinition<T>[]({ title: string, value: T }) from@/models/vuetify/SelectItemCategoryDefinitionforv-autocomplete/v-select/v-list-item. Never inline untyped{ title, value }arrays — extract to a typed constant, named for what the value represents (fooIdswhen each value is a foo id). - Never specify
item-title/item-value— Vuetify's defaults matchSelectItemCategoryDefinition. Source data with different field names is mapped to{ title, value }at the call site, never passed raw and compensated for with those props. clearableis BANNED on selects — clearing emitsnull, which violates the no-null convention and fails non-nullable API inputs. Model "no selection / all" as an explicit first item carrying the empty sentinel ({ title: "All members", value: "" },{ title: "No limit", value: 0 }) so the ref stays a plain inferredref("")/ref(0)and the sentinel propagates end-to-end (see the typescript skill's sentinel section).- Prefer enum values as display titles — set
titleto the enum member itself so key and value stay the same string, updating the enum's string value to the intended label rather than inventing a separate title. v-list-itemicon placement —prepend-iconfor decorative/category icons,append-iconfor action/severity icons (e.g. moderation actions). Action icon colour/value come from the relevantFoo*Mapconstants — never hardcoded inline.
Forms
- Name a form validity ref
isEditFormValid, bind it viav-modelon<v-form>, and initref(true)(optimistic). Prevent invalid submission through validation rules so state stays consistent, rather than catching in the submit handler (see theerror-handlingskill). StyledFormDialogconsumers never pass!isEditFormValid— it merges form validity,isSubmitting,type="submit",formandloadinginto the confirm button internally, soconfirmButtonAttrscarries only the consumer's own extra condition.StyledEditFormDialoghas noconfirmButtonAttrsat all.- Use the auto-imported
useVRules()— declareconst rules = useVRules();at the top of<script setup>with the other composables, then reference builders::rules="[rules.required(), rules.maxLength(100)]". One-off inline arrow rules in the template are fine; extract to script only when shared or unwieldy. - A built-in alias first, always (
required,maxLength,minLength,email,pattern,notEmpty, …) — never reimplement one or its message, including as a rule that surfaces a server Zod schema's issue text. A custom alias is earned only where no built-in covers the check, and is then named and worded in Vuetify's own voice (minValuebesideminLength) as a literal: routing it through the locale forces the wholeenlocale to be merged eagerly, which the app has no use for while i18n is deferred. TherequiredHTML attribute is not a Vuetify prop — use:rules="[rules.required()]". - Rules validate what is submitted, not what was typed — when the sent value is composed from the field (markup wrapper, appended link/suffix), the rule checks the composed value's constraint, even though
counterstill tracks the raw input. - Rules depending on reactive component state (uniqueness against a live list) are not global aliases — they belong in a composable, or an Ajv keyword when the form is Vjsf. See the
vue-composable-patternsskill's "Validation Rules — Pick the Right Layer".
Snackbars
- A snackbar reporting standing state takes
SNACKBAR_PERSISTENT_TIMEOUT(@/services/vuetify/constants) — an error waiting to be read, a list scrolled away from the present. Vuetify's default timeout retracts the message while what it reports is still true, and with a one-way:model-valuebinding nothing brings it back until the value flips. A timeout belongs only to a snackbar announcing something that happened.
A Dialog Born Open Waits for Its Mount
StyledDialog gates its own model on useMounted(), so consumers pass their open state straight through. A raw v-dialog rendered open on its first render — one a page's async setup decides, or one that is the page — owes the same gate: :model-value="isMounted". Vuetify's block scroll strategy reads the overlay's root element on a timeout after it activates, and a navigation renders the incoming page inside a suspense that has not mounted one yet, so the strategy dereferences undefined and the whole page render goes with it (still unguarded upstream at 4.2.0).
HTML Footprint
Prefer Vuetify components over raw HTML — avoid <div>, <span>, <p>, <ul>, <li> unless there is genuinely no suitable component: v-container/v-row/v-col for layout, v-list/v-list-item for lists (the #append slot centers inline actions), v-alert/v-messages for inline text. Only reach for raw HTML when Vuetify would add unnecessary complexity (a single unstyled text node inside a slot).
No SASS Variables in Component Styles
Never use Vuetify SASS variables ($border-width-root etc.) in component <style> blocks — they are build-time variables requiring additionalData injection, which conflicts with Vuetify's compilation pipeline. Shared values are CSS custom properties in the :root block in globals.scss; use var(--name).
The goal is always attributify: prefer inline UnoCSS utilities and delete the style block (<div b-1 b-border top="[var(--app-bar-height)]" />). The var(...) form is for the cases where a block is genuinely required — which ones those are, and when lang="scss" is earned, is the styling skill's.
Deep Dives
references/form-dialogs-and-rules.md— when wiring a form dialog or inline form's validity/error icon, choosing between a built-in rule and a custom one, or adding a custom global validation rule and wording its message.references/select-item-construction.md— when building the items constant for a select, list or menu from an enum or map.references/css-custom-properties.md— when a component genuinely needs a<style>block and a shared value in it.references/scrollspy-sub-nav.md— when a Vuetify sidebar must track which section is scrolled into view.references/nested-activators.md— when one control activates two or more overlays and no primitive covers the stack.references/button-backgrounds.md— when a button's fill is not what you expected, or a container has turned it transparent.references/router-driven-highlighting.md— when bindingtoon a button or tab, or the wrong tab is lit.references/styled-primitives.md— when building a keyboard-navigable list, rendering a user's avatar, or reaching for a shared wrapper around a tooltip and a text button.
Signals
- GitHub stars
- 23
- Forks
- 3
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
vuetify- Source
- github.com/esposter/esposter