UnoCSS with presetWind4
Use @unocss/preset-wind4 for TailwindCSS v4 class names powered by UnoCSS’s on-demand engine.
Unlike TailwindCSS v4 directly (which requires pure CSS @theme declarations), everything stays in JavaScript/TypeScript — breakpoints, typography, dark mode, all in one shared config file.
# generate working project for reference
npx @vuetify/cli@latest init --type=vuetify --css=unocss-wind4
Establish CSS layer order
Create a layers.css file that declares the cascade layers in order. uno goes above component styles but below vuetify-final, where Vuetify keeps its transitions:
@layer uno-base;
@layer uno-theme;
@layer vuetify-core;
@layer vuetify-components;
@layer vuetify-overrides;
@layer vuetify-utilities;
@layer uno-shortcuts;
@layer uno-default;
@layer vuetify-final;
This file must be loaded before any other styles. In a Vite project, save it as src/styles/layers.css and import it at the top of src/plugins/vuetify.ts, before vuetify/styles.
Setup dependencies
Vite
Import the layers file at the top of src/plugins/vuetify.ts, before vuetify/styles:
import '../styles/layers.css'
import 'vuetify/styles'
// ...
Install UnoCSS and the Wind4 preset:
pnpm add -D unocss @unocss/preset-wind4
Register the UnoCSS Vite plugin in vite.config.ts and create uno.config.ts:
import UnoCSS from 'unocss/vite'
export default defineConfig({
plugins: [
UnoCSS(),
// ...
],
})
import { defineConfig } from 'unocss'
import presetWind4 from '@unocss/preset-wind4'
export default defineConfig({
presets: [
presetWind4(),
],
outputToCssLayers: {
cssLayerName: (layer) => layer === 'properties' ? null : `uno-${layer}`,
},
})
Setting preflights.reset to false skips UnoCSS’s built-in reset so Vuetify’s takes over.
Add the UnoCSS virtual import to your entry point:
import 'virtual:uno.css'
Nuxt
pnpm add -D unocss @unocss/preset-wind4 @unocss/nuxt
Register the module in nuxt.config.ts. The css array controls load order — layers.css must come first, followed by vuetify/styles. Set disableVuetifyStyles: true — otherwise the module injects styles automatically and the order above is ignored:
import presetWind4 from '@unocss/preset-wind4'
export default defineNuxtConfig({
modules: [
'@unocss/nuxt',
'vuetify-nuxt-module',
// ...
],
css: [
'assets/styles/layers.css',
'vuetify/styles',
],
vuetify: {
moduleOptions: {
disableVuetifyStyles: true,
styles: { configFile: 'assets/styles/settings.scss' },
},
},
unocss: {
presets: [
presetWind4(),
],
outputToCssLayers: {
cssLayerName: (layer) => layer === 'properties' ? null : `uno-${layer}`,
},
},
})
Disable Vuetify’s built-in utilities
Turn off Vuetify’s built-in utility classes so UnoCSS handles them on demand instead. The Material color palette is not needed because presetWind4 ships its own color palette (TailwindCSS colors).
@use 'vuetify/settings' with (
$color-pack: false,
$utilities: false,
);
Light/dark mode compatibility
By default UnoCSS generates dark-mode utilities scoped to a .dark class (e.g. .dark .dark\:bg-sky-900). Vuetify uses .v-theme--dark and .v-theme--light instead, so the dark: prefix won’t work out of the box.
Add the dark option inside presetWind4() to align both systems:
presetWind4({
preflights: {
reset: false,
},
dark: {
dark: '.v-theme--dark',
light: '.v-theme--light',
},
}),
Classes like dark:bg-sky-900 and light:text-gray-700 are now scoped to Vuetify’s theme selectors and toggle correctly via $vuetify.theme.cycle() or programmatically.
Custom themes
TailwindCSS only supports the light and dark themes to align with prefers-color-scheme options. If your app registers additional custom themes and you want variant prefixes for each of them, use createThemeVariants from unocss-preset-vuetify instead:
import { createThemeVariants } from 'unocss-preset-vuetify'
export default defineConfig({
// ...
variants: createThemeVariants(['light', 'dark', 'high-contrast']),
})
Align breakpoints
Default breakpoints from TailwindCSS do not match Vuetify’s. This mismatch can lead to confusing layout bugs when mixing responsive utilities (sm:, md:, …) with Vuetify’s grid system (v-col, v-row) or useDisplay().
Define breakpoints in a shared file and feed them to both Vuetify and UnoCSS:
import type { DisplayThresholds } from 'vuetify'
// repeated in settings.scss
const breakpoints: DisplayThresholds = {
xs: 0,
sm: 600,
md: 960,
lg: 1280,
xl: 1920,
xxl: 2560,
}
export const forVuetify = breakpoints
export const forUnoCSS = Object.entries(breakpoints)
.reduce(
(o, [key, value]) => ({ ...o, [key]: `${value}px` }),
{} as Record<keyof DisplayThresholds, string>,
)
Apply the breakpoints in your UnoCSS configuration:
// uno.config.ts (Vite) or unocss key (Nuxt)
theme: {
breakpoint: breakpoints.forUnoCSS,
},
And in your Vuetify configuration:
// vuetify plugin (Vite) or vuetifyOptions (Nuxt)
display: {
mobileBreakpoint: 'md',
thresholds: breakpoints.forVuetify,
},
Finally, keep the SCSS variables in sync:
@use 'vuetify/settings' with (
$color-pack: false,
$utilities: false,
$grid-breakpoints: (
// repeated in breakpoints.ts
'xs': 0,
'sm': 600px,
'md': 960px,
'lg': 1280px,
'xl': 1920px,
'xxl': 2560px,
),
);
Typography
presetWind4 does not include Vuetify’s typography classes (text-h1 through text-overline). You can recreate them as UnoCSS shortcuts so they compose from native TailwindCSS utilities and gain responsive prefix support (e.g. md:text-h3).
First, define custom font families in the UnoCSS theme so the font-heading and font-body utilities are available:
theme: {
font: {
heading: "'Your Heading Font', sans-serif",
body: "'Your Body Font', sans-serif",
},
},
Then point Vuetify’s Sass variables at the same CSS custom properties:
@use 'vuetify/settings' with (
$heading-font-family: var(--font-heading),
$body-font-family: var(--font-body),
// ...
);
MD2 typography shortcuts (text-h1 … text-overline)
Add the shortcuts below to your UnoCSS configuration. The values are matched to Vuetify’s MD2 typography scale:
shortcuts: {
'text-h1': ' font-heading normal-case text-[6rem] font-[300] leading-[1] tracking-[-.015625em] ',
'text-h2': ' font-heading normal-case text-[3.75rem] font-[300] leading-[1] tracking-[-.0083333333em]',
'text-h3': ' font-heading normal-case text-[3rem] font-[400] leading-[1.05] tracking-[normal] ',
'text-h4': ' font-heading normal-case text-[2.125rem] font-[400] leading-[1.175] tracking-[.0073529412em] ',
'text-h5': ' font-heading normal-case text-[1.5rem] font-[400] leading-[1.333] tracking-[normal] ',
'text-h6': ' font-heading normal-case text-[1.25rem] font-[500] leading-[1.6] tracking-[.0125em] ',
'text-subtitle-1': 'font-body normal-case text-[1rem] font-[400] leading-[1.75] tracking-[.009375em] ',
'text-subtitle-2': 'font-body normal-case text-[.875rem] font-[500] leading-[1.6] tracking-[.0071428571em] ',
'text-body-1': ' font-body normal-case text-[1rem] font-[400] leading-[1.5] tracking-[.03125em] ',
'text-body-2': ' font-body normal-case text-[.875rem] font-[400] leading-[1.425] tracking-[.0178571429em] ',
'text-button': ' font-body uppercase text-[.875rem] font-[500] leading-[2.6] tracking-[.0892857143em] ',
'text-caption': ' font-body normal-case text-[.75rem] font-[400] leading-[1.667] tracking-[.0333333333em] ',
'text-overline': ' font-body uppercase text-[.75rem] font-[500] leading-[2.667] tracking-[.1666666667em] ',
},
Because these are shortcuts composed from real utilities, responsive prefixes like sm:text-h3 work automatically.
MD3 typography shortcuts (text-display-large … text-label-small)
These match Vuetify’s MD3 defaults (the current default typography scale). None of the MD3 classes use text-transform.
shortcuts: {
'text-display-large': ' font-heading normal-case text-[3.5625rem] font-[400] leading-[1.1228] tracking-[-.0044em]',
'text-display-medium': ' font-heading normal-case text-[2.8125rem] font-[400] leading-[1.1556] tracking-[normal] ',
'text-display-small': ' font-heading normal-case text-[2.25rem] font-[400] leading-[1.2222] tracking-[normal] ',
'text-headline-large': ' font-heading normal-case text-[2rem] font-[400] leading-[1.25] tracking-[normal] ',
'text-headline-medium': 'font-heading normal-case text-[1.75rem] font-[400] leading-[1.2857] tracking-[normal] ',
'text-headline-small': ' font-heading normal-case text-[1.5rem] font-[400] leading-[1.3333] tracking-[normal] ',
'text-title-large': ' font-heading normal-case text-[1.375rem] font-[400] leading-[1.2727] tracking-[normal] ',
'text-title-medium': ' font-body normal-case text-[1rem] font-[500] leading-[1.5] tracking-[.0094em] ',
'text-title-small': ' font-body normal-case text-[.875rem] font-[500] leading-[1.4286] tracking-[.0071em] ',
'text-body-large': ' font-body normal-case text-[1rem] font-[400] leading-[1.5] tracking-[.0313em] ',
'text-body-medium': ' font-body normal-case text-[.875rem] font-[400] leading-[1.4286] tracking-[.0179em] ',
'text-body-small': ' font-body normal-case text-[.75rem] font-[400] leading-[1.3333] tracking-[.0333em] ',
'text-label-large': ' font-body normal-case text-[.875rem] font-[500] leading-[1.4286] tracking-[.0071em] ',
'text-label-medium': ' font-body normal-case text-[.75rem] font-[500] leading-[1.3333] tracking-[.0417em] ',
'text-label-small': ' font-body normal-case text-[.6875rem] font-[500] leading-[1.4545] tracking-[.0455em] ',
},
Rounded corners
Vuetify’s rounded prop generates classes like rounded-lg and rounded-pill that don’t map to TailwindCSS equivalents. Define them as UnoCSS shortcuts so they work with responsive prefixes and don’t need safelisting:
Rounded shortcuts (aligned with Vuetify defaults)
shortcuts: {
// typography shortcuts ...
'rounded-0': 'rounded-none',
'rounded-sm': 'rounded-[2px]',
'rounded': 'rounded-[4px]',
'rounded-lg': 'rounded-[8px]',
'rounded-xl': 'rounded-[24px]',
'rounded-pill': 'rounded-full',
'rounded-circle': 'rounded-[50%]',
'rounded-shaped': 'rounded-[24px_0]',
// directional variants
'rounded-t-0': 'rounded-t-none',
'rounded-t-sm': 'rounded-tl-[2px] rounded-tr-[2px]',
'rounded-t': 'rounded-tl-[4px] rounded-tr-[4px]',
'rounded-t-lg': 'rounded-tl-[8px] rounded-tr-[8px]',
'rounded-t-xl': 'rounded-tl-[24px] rounded-tr-[24px]',
'rounded-t-pill': 'rounded-tl-full rounded-tr-full',
'rounded-b-0': 'rounded-b-none',
'rounded-b-sm': 'rounded-bl-[2px] rounded-br-[2px]',
'rounded-b': 'rounded-bl-[4px] rounded-br-[4px]',
'rounded-b-lg': 'rounded-bl-[8px] rounded-br-[8px]',
'rounded-b-xl': 'rounded-bl-[24px] rounded-br-[24px]',
'rounded-b-pill': 'rounded-bl-full rounded-br-full',
'rounded-s-0': 'rounded-ss-none rounded-es-none',
'rounded-s-sm': 'rounded-ss-[2px] rounded-es-[2px]',
'rounded-s': 'rounded-ss-[4px] rounded-es-[4px]',
'rounded-s-lg': 'rounded-ss-[8px] rounded-es-[8px]',
'rounded-s-xl': 'rounded-ss-[24px] rounded-es-[24px]',
'rounded-s-pill': 'rounded-ss-full rounded-es-full',
'rounded-e-0': 'rounded-se-none rounded-ee-none',
'rounded-e-sm': 'rounded-se-[2px] rounded-ee-[2px]',
'rounded-e': 'rounded-se-[4px] rounded-ee-[4px]',
'rounded-e-lg': 'rounded-se-[8px] rounded-ee-[8px]',
'rounded-e-xl': 'rounded-se-[24px] rounded-ee-[24px]',
'rounded-e-pill': 'rounded-se-full rounded-ee-full',
},
Elevation utilities
presetWind4 does not generate Vuetify’s elevation-* classes. Choose one of the two approaches below depending on whether you want a TailwindCSS-native shadow scale or Vuetify’s exact Material Design shadows.
Align with shadows from TailwindCSS
Map each elevation level to presetWind4’s built-in shadow tokens, which use the same scale as TailwindCSS v4:
rules: [
['elevation-0', { 'box-shadow': 'none' }],
['elevation-1', { 'box-shadow': 'var(--shadow-xs)' }],
['elevation-2', { 'box-shadow': 'var(--shadow-sm)' }],
['elevation-3', { 'box-shadow': 'var(--shadow-md)' }],
['elevation-4', { 'box-shadow': 'var(--shadow-xl)' }],
['elevation-5', { 'box-shadow': 'var(--shadow-2xl)' }],
],
Many Vuetify components include default shadows that are not aligned with TailwindCSS. You may want to extend settings.scss with all variables *-elevation.
@use 'vuetify/settings' with (
// ...
$alert-elevation: 0,
$avatar-elevation: 0,
$card-elevation: 0,
$card-hover-elevation: 0,
// etc.
);
Restore Vuetify elevation shadows
If you find TailwindCSS shadows do not align with the app design and wish to use Material Design shadows, the easiest way is to use unocss-preset-vuetify package. Make sure you install the library as dependency and import elevationPresets which can be translated into UnoCSS “rules”.
import { elevationPresets } from 'unocss-preset-vuetify'
// ...
rules: [
...Object.entries(elevationPresets.md3)
.map(([level, css]) => [`elevation-${level}`, css]),
],
These rules let you control shadows with CSS variables bound to theme configuration (notably --v-shadow-color). You can also easily swap to md2 for legacy 24-levels of elevation.
Safelist prop-driven classes
Some Vuetify convenience props (elevation, rounded) add CSS classes at runtime. This means that these class names may never appear in your source files and if UnoCSS cannot detect them by scanning, they won’t appear in the CSS bundle file. Add safelist entries for the ones you use:
{
// presets: ...
// outputToCssLayers: ...
safelist: [
...Array.from({ length: 6 /* or 25 for MD2 */ }, (_, i) => `elevation-${i}`),
['', '-0', '-sm', '-lg', '-xl', '-pill', '-circle', '-shaped'].map(suffix => `rounded${suffix}`),
],
}
VRow and VCol utility classes
This section is only relevant if your project uses the justify, align, or order props on v-row / v-col — these props were deprecated in Vuetify v4.0.0 — and migrating them to their TailwindCSS equivalents (which sometimes use different class names) is not practical for your project.
v-row and v-col rely on Vuetify-specific utility classes for their justify, align, and order props (e.g. justify-space-between, align-center). These class names don’t exist in TailwindCSS conventions and won’t be generated by presetWind4.
You have two options:
Option A — static global styles. Paste the required classes in a global CSS/SCSS file:
.justify-start { justify-content: flex-start }
.justify-end { justify-content: flex-end }
.justify-center { justify-content: center }
.justify-space-between { justify-content: space-between }
.justify-space-around { justify-content: space-around }
.justify-space-evenly { justify-content: space-evenly }
.align-start { align-items: flex-start }
.align-end { align-items: flex-end }
.align-center { align-items: center }
.align-baseline { align-items: baseline }
.align-stretch { align-items: stretch }
Option B — keep selected Vuetify utilities. Instead of $utilities: false, selectively enable only the classes v-row / v-col need:
@use 'vuetify/settings' with (
$color-pack: false,
$utilities: (
"align-items": (responsive: false, unimportant: (align-items)),
"justify-content": (responsive: false, unimportant: (justify-content)),
"order": (responsive: false, unimportant: (order)),
// all other utilities set to `false`
),
);
Forward Vuetify theme colors to UnoCSS
Vuetify stores theme colors as raw RGB channels in CSS custom properties (e.g. --v-theme-primary). Wrapping them in rgb() inside the UnoCSS theme.colors block makes them available as standard TailwindCSS-style color utilities (bg-primary, text-error, etc.):
theme: {
colors: {
background: 'rgb(var(--v-theme-background))',
surface: 'rgb(var(--v-theme-surface))',
'surface-variant': 'rgb(var(--v-theme-surface-variant))',
primary: 'rgb(var(--v-theme-primary))',
success: 'rgb(var(--v-theme-success))',
warning: 'rgb(var(--v-theme-warning))',
error: 'rgb(var(--v-theme-error))',
info: 'rgb(var(--v-theme-info))',
},
},
Because presetWind4 generates utilities on demand, also add safelist entries for any color classes that are bound dynamically via color="..." prop:
safelist: [
'bg-primary', 'text-primary',
'bg-success', 'text-success',
'bg-error', 'text-error',
],
Vuetify’s original bg-* utilities automatically set a contrasting foreground color via --v-theme-on-*. Replacing them with UnoCSS utilities removes this safeguard — you are responsible for choosing legible text colors. See Limitations.