Back to Vuetify

TailwindCSS

packages/docs/src/pages/en/features/css-utilities/tailwindcss.md

4.0.817.9 KB
Original Source

TailwindCSS

Integrate TailwindCSS v4 into an existing Vuetify project for a smaller CSS bundle, on-demand utility generation, and variants like hover:, dark:, and responsive prefixes.

<PageFeatures /> <PromotedEntry />

:::: tabs

bash
# generate working project for reference
npx @vuetify/cli@latest init --type=vuetify --css=tailwindcss
bash
# generate working project for reference
pnpx @vuetify/cli@latest init --type=vuetify --css=tailwindcss
bash
# generate working project for reference
yarn dlx @vuetify/cli@latest init --type=vuetify --css=tailwindcss
bash
# generate working project for reference
bunx @vuetify/cli@latest init --type=vuetify --css=tailwindcss

::::

Establish CSS layer order

Create a layers.css file that declares the cascade layers in order. tailwind goes above component styles but below vuetify-final, where Vuetify keeps its transitions:

css
@layer tailwind-theme;
@layer tailwind-reset;

@layer vuetify-core;
@layer vuetify-components;
@layer vuetify-overrides;
@layer vuetify-utilities;

@layer tailwind-utilities;

@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. You can find the exact configuration snippets in the sections for Vite and Nuxt below.

Setup dependencies

Vite

Import the layers file at the top of src/plugins/vuetify.ts, before vuetify/styles:

ts
import '../styles/layers.css'
import 'vuetify/styles'
// ...

Install TailwindCSS and the Vite plugin:

::: tabs

bash
pnpm add -D tailwindcss @tailwindcss/vite
bash
yarn add -D tailwindcss @tailwindcss/vite
bash
npm i -D tailwindcss @tailwindcss/vite
bash
bun add -D tailwindcss @tailwindcss/vite

:::

Register tailwindcss() as the first entry in plugins inside vite.config.mts:

ts
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    tailwindcss(), // must come before Vuetify
    // ...
  ],
})

Import the TailwindCSS stylesheet (see Configure TailwindCSS) in src/main.ts:

ts
import './styles/tailwind.css'

Nuxt

::: tabs

bash
pnpm add -D tailwindcss @tailwindcss/postcss
bash
yarn add -D tailwindcss @tailwindcss/postcss
bash
npm i -D tailwindcss @tailwindcss/postcss
bash
bun add -D tailwindcss @tailwindcss/postcss

:::

Register @tailwindcss/postcss as a PostCSS plugin in nuxt.config.ts. The css array controls load order — layers.css must come first, followed by vuetify/styles, then tailwind.css. Set disableVuetifyStyles: true — otherwise the module injects styles automatically and the order above is ignored:

ts
export default defineNuxtConfig({
  modules: [
    'vuetify-nuxt-module',
    // ...
  ],

  css: [
    'assets/styles/layers.css',
    'vuetify/styles',
    'assets/styles/tailwind.css',
  ],

  postcss: {
    plugins: {
      '@tailwindcss/postcss': {},
    },
  },

  vuetify: {
    moduleOptions: {
      disableVuetifyStyles: true,
      styles: { configFile: 'assets/styles/settings.scss' },
    },
  },
})

Configure TailwindCSS

Create tailwind.css (in src/styles/ for Vite or assets/styles/ for Nuxt). Tailwind's preflight is skipped because Vuetify ships its own reset. The @custom-variant declarations wire dark: and light: prefixes to Vuetify's theme classes, and breakpoints are overridden to match Vuetify's defaults:

css
@import "tailwindcss/theme" layer(tailwind-theme);
@import "tailwindcss/preflight" layer(tailwind-reset);
@import "tailwindcss/utilities" layer(tailwind-utilities);

/* dark/light mode — Vuetify uses .v-theme--dark/.v-theme--light instead of .dark */
@custom-variant light (&:where(.v-theme--light, .v-theme--light *));
@custom-variant dark  (&:where(.v-theme--dark,  .v-theme--dark  *));

@theme {
  --breakpoint-*: initial; /* reset Tailwind defaults */
  /* keep in sync with vuetify plugin/config and settings.scss */
  --breakpoint-xs:  0px;
  --breakpoint-sm:  600px;
  --breakpoint-md:  960px;
  --breakpoint-lg:  1280px;
  --breakpoint-xl:  1920px;
  --breakpoint-xxl: 2560px;
}

/*
  note: adopt and extend values from TailwindCSS
*/
@utility rounded-pill   { border-radius: 9999px }
@utility rounded-circle { border-radius: 50%    }
@utility rounded-shaped { border-radius: 24px 0 }

@source inline('rounded'); /* .25rem */
@source inline('rounded-{none,sm,md,lg,xl,2xl,3xl,full,pill,circle,shaped}');

/*
  note: adopt elevation shadows from TailwindCSS
*/
@utility elevation-0 { box-shadow: none }
@utility elevation-1 { box-shadow: var(--shadow-xs) }
@utility elevation-2 { box-shadow: var(--shadow-sm) }
@utility elevation-3 { box-shadow: var(--shadow-md) }
@utility elevation-4 { box-shadow: var(--shadow-xl) }
@utility elevation-5 { box-shadow: var(--shadow-2xl) }

@source inline('elevation-{0,1,2,3,4,5}');

Disable Vuetify's built-in utilities

Turn off Vuetify's built-in utility classes and the Material color palette — TailwindCSS will cover both from here on:

scss
@use 'vuetify/settings' with (
  $color-pack: false,
  $utilities: false,
);

After rebuilding you should see the CSS entry file shrink by roughly 150–200 kB (unminified).

Align breakpoints { id="breakpoints" }

Vuetify and TailwindCSS ship different default breakpoints. Mismatched values cause sm: in Tailwind to fire at a different width than sm in VCol or useDisplay(). The @theme block in tailwind.css above already resets the Tailwind defaults — the same values need to be repeated in two more places.

Vuetify plugin (Vite) or vuetifyOptions (Nuxt):

ts
display: {
  mobileBreakpoint: 'md',
  thresholds: {
    // repeated in tailwind.css and settings.scss
    xs: 0, sm: 600, md: 960, lg: 1280, xl: 1920, xxl: 2560,
  },
},

Sass variables:

scss
@use 'vuetify/settings' with (
  $color-pack: false,
  $utilities: false,
  $grid-breakpoints: (
    // repeated in tailwind.css and vuetify config
    'xs': 0,
    'sm': 600px,
    'md': 960px,
    'lg': 1280px,
    'xl': 1920px,
    'xxl': 2560px,
  ),
);

::: tip In a Nuxt project you can define breakpoints in a shared TypeScript file and feed them to both Vuetify and the Sass variables from a single source of truth. See the UnoCSS presetWind4 guide for an example of this pattern. :::

Dark mode { id="dark-mode" }

The @custom-variant declarations in tailwind.css above rewire Tailwind's dark: and light: prefixes to Vuetify's theme selectors. Classes like dark:bg-sky-900 and light:text-gray-700 then toggle correctly when switching themes via $vuetify.theme.cycle() or programmatically.

::: warning Nuxt SSR and defaultTheme: 'system'

Vuetify's system theme reads the browser's prefers-color-scheme media query at runtime. In a Nuxt project with SSR enabled (the default), this detection runs on the server where no browser preference is available, so the theme always falls back to light. Either add ssr: false to nuxt.config.ts, or start with a fixed default theme:

ts
theme: {
  defaultTheme: 'dark', // or 'light' — 'system' requires ssr: false
},

:::

Typography { id="typography" }

TailwindCSS provides its own type-scale utilities (text-sm, text-base, text-2xl, etc.) that work well with responsive prefixes. When adopting TailwindCSS, migrate to this convention rather than trying to preserve Vuetify's typography classes (text-h1 through text-overline for MD2, or text-display-large through text-label-small for MD3).

TailwindCSS utilities give you finer control — text-2xl font-light tracking-tight lets you mix size, weight, and spacing freely instead of relying on a predefined bundle. The trade-off is that your team needs to agree on which combinations to use, usually enforced through shared components or a design token system.

If you are integrating TailwindCSS into an existing project that already uses Vuetify's typography classes extensively, you can define @utility rules to keep them working during the migration. Below are drop-in snippets for both the MD2 (legacy) and MD3 (default) typography scales.

<details> <summary>MD2 typography utilities (text-h1 … text-overline)</summary>

These match Vuetify's MD2 defaults and reference --font-heading / --font-body CSS custom properties. Define them in your global CSS or in a Sass @use 'vuetify/settings' block.

css
@utility text-h1 {
  font-family: var(--font-heading, inherit);
  font-size: 6rem;
  font-weight: 300;
  line-height: 1;
  letter-spacing: -.015625em;
  text-transform: none;
}
@utility text-h2 {
  font-family: var(--font-heading, inherit);
  font-size: 3.75rem;
  font-weight: 300;
  line-height: 1;
  letter-spacing: -.0083333333em;
  text-transform: none;
}
@utility text-h3 {
  font-family: var(--font-heading, inherit);
  font-size: 3rem;
  font-weight: 400;
  line-height: 1.05;
  letter-spacing: normal;
  text-transform: none;
}
@utility text-h4 {
  font-family: var(--font-heading, inherit);
  font-size: 2.125rem;
  font-weight: 400;
  line-height: 1.175;
  letter-spacing: .0073529412em;
  text-transform: none;
}
@utility text-h5 {
  font-family: var(--font-heading, inherit);
  font-size: 1.5rem;
  font-weight: 400;
  line-height: 1.333;
  letter-spacing: normal;
  text-transform: none;
}
@utility text-h6 {
  font-family: var(--font-heading, inherit);
  font-size: 1.25rem;
  font-weight: 500;
  line-height: 1.6;
  letter-spacing: .0125em;
  text-transform: none;
}
@utility text-subtitle-1 {
  font-family: var(--font-body, inherit);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.75;
  letter-spacing: .009375em;
  text-transform: none;
}
@utility text-subtitle-2 {
  font-family: var(--font-body, inherit);
  font-size: .875rem;
  font-weight: 500;
  line-height: 1.6;
  letter-spacing: .0071428571em;
  text-transform: none;
}
@utility text-body-1 {
  font-family: var(--font-body, inherit);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: .03125em;
  text-transform: none;
}
@utility text-body-2 {
  font-family: var(--font-body, inherit);
  font-size: .875rem;
  font-weight: 400;
  line-height: 1.425;
  letter-spacing: .0178571429em;
  text-transform: none;
}
@utility text-button {
  font-family: var(--font-body, inherit);
  font-size: .875rem;
  font-weight: 500;
  line-height: 2.6;
  letter-spacing: .0892857143em;
  text-transform: uppercase;
}
@utility text-caption {
  font-family: var(--font-body, inherit);
  font-size: .75rem;
  font-weight: 400;
  line-height: 1.667;
  letter-spacing: .0333333333em;
  text-transform: none;
}
@utility text-overline {
  font-family: var(--font-body, inherit);
  font-size: .75rem;
  font-weight: 500;
  line-height: 2.667;
  letter-spacing: .1666666667em;
  text-transform: uppercase;
}
</details> <details> <summary>MD3 typography utilities (text-display-large … text-label-small)</summary>

These match Vuetify's MD3 defaults (the current default typography scale). None of the MD3 classes use text-transform.

css
@utility text-display-large {
  font-family: var(--font-heading, inherit);
  font-size: 3.5625rem;
  font-weight: 400;
  line-height: 1.1228070175;
  letter-spacing: -.0043859649em;
}
@utility text-display-medium {
  font-family: var(--font-heading, inherit);
  font-size: 2.8125rem;
  font-weight: 400;
  line-height: 1.1555555556;
  letter-spacing: normal;
}
@utility text-display-small {
  font-family: var(--font-heading, inherit);
  font-size: 2.25rem;
  font-weight: 400;
  line-height: 1.2222222222;
  letter-spacing: normal;
}
@utility text-headline-large {
  font-family: var(--font-heading, inherit);
  font-size: 2rem;
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: normal;
}
@utility text-headline-medium {
  font-family: var(--font-heading, inherit);
  font-size: 1.75rem;
  font-weight: 400;
  line-height: 1.2857142857;
  letter-spacing: normal;
}
@utility text-headline-small {
  font-family: var(--font-heading, inherit);
  font-size: 1.5rem;
  font-weight: 400;
  line-height: 1.3333333333;
  letter-spacing: normal;
}
@utility text-title-large {
  font-family: var(--font-heading, inherit);
  font-size: 1.375rem;
  font-weight: 400;
  line-height: 1.2727272727;
  letter-spacing: normal;
}
@utility text-title-medium {
  font-family: var(--font-body, inherit);
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: .009375em;
}
@utility text-title-small {
  font-family: var(--font-body, inherit);
  font-size: .875rem;
  font-weight: 500;
  line-height: 1.4285714286;
  letter-spacing: .0071428571em;
}
@utility text-body-large {
  font-family: var(--font-body, inherit);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: .03125em;
}
@utility text-body-medium {
  font-family: var(--font-body, inherit);
  font-size: .875rem;
  font-weight: 400;
  line-height: 1.4285714286;
  letter-spacing: .0178571429em;
}
@utility text-body-small {
  font-family: var(--font-body, inherit);
  font-size: .75rem;
  font-weight: 400;
  line-height: 1.3333333333;
  letter-spacing: .0333333333em;
}
@utility text-label-large {
  font-family: var(--font-body, inherit);
  font-size: .875rem;
  font-weight: 500;
  line-height: 1.4285714286;
  letter-spacing: .0071428571em;
}
@utility text-label-medium {
  font-family: var(--font-body, inherit);
  font-size: .75rem;
  font-weight: 500;
  line-height: 1.3333333333;
  letter-spacing: .0416666667em;
}
@utility text-label-small {
  font-family: var(--font-body, inherit);
  font-size: .6875rem;
  font-weight: 500;
  line-height: 1.4545454545;
  letter-spacing: .0454545455em;
}
</details>

Theme colors { id="theme-colors" }

Vuetify stores theme colors as raw RGB channels in CSS custom properties (e.g. --v-theme-primary). Wrapping them in rgb() inside a @theme block makes them available as standard Tailwind color utilities (bg-primary, text-error, etc.):

css
@theme {
  /* ... breakpoints ... */
  --color-background:      rgb(var(--v-theme-background));
  --color-surface:         rgb(var(--v-theme-surface));
  --color-surface-variant: rgb(var(--v-theme-surface-variant));
  --color-primary:         rgb(var(--v-theme-primary));
  --color-success:         rgb(var(--v-theme-success));
  --color-warning:         rgb(var(--v-theme-warning));
  --color-error:           rgb(var(--v-theme-error));
  --color-info:            rgb(var(--v-theme-info));
}

/* safelist classes used dynamically via color="..." prop */
@source inline('bg-primary');
@source inline('text-primary');
@source inline('bg-success');
@source inline('text-success');
@source inline('bg-error');
@source inline('text-error');

Disable Vuetify's own theme utility classes to avoid duplicate bg-* / text-* rules that can't be used with variants:

ts
theme: {
  defaultTheme: 'light', // 'system' requires ssr: false in Nuxt
  utilities: false, // skip .bg-primary, .text-error, etc.
},

::: warning

Vuetify's original bg-* utilities automatically set a contrasting foreground color via --v-theme-on-*. Replacing them with TailwindCSS utilities removes this safeguard — you are responsible for choosing legible text colors. See Limitations.

:::

Using MD3 elevation

The elevation approach in Configure TailwindCSS maps elevation-* to TailwindCSS's generic shadow tokens. If you need Vuetify's exact Material Design 3 shadow values — including the surface overlay tint that shifts with depth — replace those rules with the full MD3 definitions:

<details> <summary>MD3 elevation utilities (elevation-0 … elevation-5)</summary>
css
@utility elevation-0 {
  box-shadow: 0px 0px 0px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 0px 0px 0px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
  --v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 0%, transparent);
}
@utility elevation-1 {
  box-shadow: 0px 1px 2px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 1px 3px 1px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
  --v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 2%, transparent);
}
@utility elevation-2 {
  box-shadow: 0px 1px 2px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 2px 6px 2px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
  --v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 4%, transparent);
}
@utility elevation-3 {
  box-shadow: 0px 1px 3px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 4px 8px 3px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
  --v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 6%, transparent);
}
@utility elevation-4 {
  box-shadow: 0px 2px 3px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 6px 10px 4px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
  --v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 8%, transparent);
}
@utility elevation-5 {
  box-shadow: 0px 4px 4px 0px rgba(var(--v-shadow-color), var(--v-shadow-key-opacity, 0.3)), 0px 8px 12px 6px rgba(var(--v-shadow-color), var(--v-shadow-ambient-opacity, 0.15));
  --v-elevation-overlay: color-mix(in srgb, var(--v-elevation-overlay-color) 10%, transparent);
}
@utility elevation-overlay {
  background-image: linear-gradient(var(--v-elevation-overlay), var(--v-elevation-overlay));
}
</details>