BlogPlaygroundOne

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.

Edit this page
Copy Page as Markdown

# 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:

src/plugins/vuetify.ts
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:

vite.config.ts
import UnoCSS from 'unocss/vite'

export default defineConfig({
  plugins: [
    UnoCSS(),
    // ...
  ],
})
uno.config.ts
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:

src/main.ts
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:

nuxt.config.ts
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).

settings.scss
@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:

uno.config.ts
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:

uno.config.ts
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:

src/theme/breakpoints.ts
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:

settings.scss
@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:

uno.config.ts
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:

settings.scss
@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:

uno.config.ts or nuxt.config.ts » unocss
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.

uno.config.ts or nuxt.config.ts » unocss
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)
uno.config.ts or nuxt.config.ts » unocss
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:

uno.config.ts or nuxt.config.ts » unocss
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.

settings.scss
@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”.

uno.config.ts or nuxt.config.ts » unocss
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:

src/styles/vuetify-compat.scss
.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:

settings.scss
@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.):

uno.config.ts
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',
],

Ready for more?

Continue your learning with related content selected by the Team or move between pages by using the navigation links below.