Tailwind CSS with Nuxt: Setup, Configuration, and Best Practices
Everything you need to set up Tailwind CSS in a Nuxt application — from initial config to design tokens, component patterns, dark mode, and keeping your classes maintainable.

James Ross Jr.
Strategic Systems Architect & Enterprise Software Developer
Tailwind CSS and Nuxt are a natural pairing. Both embrace a component-based mental model, both have excellent TypeScript support, and both generate optimized output for production. But I see a consistent pattern: developers set up Tailwind correctly and then gradually let the codebase degrade into a mess of repeated class strings and undocumented magic numbers.
This guide covers not just the setup — which is honestly straightforward — but the practices that keep a Tailwind codebase maintainable over the lifetime of a project.
Setup
Nuxt has a first-party Tailwind module that handles the configuration:
npx nuxi module add tailwindcss
This installs @nuxtjs/tailwindcss, adds it to nuxt.config.ts, and creates a tailwind.config.ts file. For new projects, that is all you need to start using Tailwind classes.
The module automatically scans your Nuxt directories — components/, pages/, layouts/, composables/ — for class usage and generates an optimized CSS bundle that includes only the classes you use. No manual purge configuration needed.
Tailwind Configuration
The tailwind.config.ts file is where you customize Tailwind to match your design system:
import type { Config } from 'tailwindcss'
export default {
content: [], // Nuxt module handles this
theme: {
extend: {
colors: {
brand: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
},
fontSize: {
'2xs': '0.625rem',
},
spacing: {
'18': '4.5rem',
'112': '28rem',
'128': '32rem',
},
},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
require('@tailwindcss/aspect-ratio'),
],
} satisfies Config
The extend key adds to Tailwind's defaults rather than replacing them. Use this for your custom tokens — do not replace Tailwind's default color palette unless you have a very specific reason.
Design Tokens as Configuration
The most important practice for a maintainable Tailwind codebase is encoding your design decisions in tailwind.config.ts, not in component class strings. If your brand color is a specific blue, define it once in configuration:
colors: {
brand: '#2563eb',
}
Now you write bg-brand everywhere instead of bg-[#2563eb]. When the brand color changes (and it will), you change one line in tailwind.config.ts.
This applies to spacing too. If your layout has a consistent sidebar width of 280px, define it:
spacing: {
sidebar: '280px',
}
Then use w-sidebar in your layout components instead of w-[280px].
Component Patterns That Stay Clean
The most common Tailwind codebase smell is long, repeated class strings. A button rendered in 15 places with the same 8 classes is a maintenance problem. Extract it:
<!-- components/AppButton.vue -->
<script setup lang="ts">
interface Props {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
size?: 'sm' | 'md' | 'lg'
loading?: boolean
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
loading: false,
disabled: false,
})
const variantClasses = {
primary: 'bg-brand-600 text-white hover:bg-brand-700 focus:ring-brand-500',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-400',
ghost: 'bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-400',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
}
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-5 py-2.5 text-base',
}
</script>
<template>
<button
:class="[
'inline-flex items-center justify-center font-medium rounded-lg',
'transition-colors duration-150',
'focus:outline-none focus:ring-2 focus:ring-offset-2',
'disabled:opacity-50 disabled:cursor-not-allowed',
variantClasses[variant],
sizeClasses[size],
]"
:disabled="disabled || loading"
v-bind="$attrs"
>
<span v-if="loading" class="mr-2">
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24" aria-hidden="true">
<!-- spinner SVG -->
</svg>
</span>
<slot />
</button>
</template>
This pattern keeps the class logic in one place, makes variants explicit, and gives you a clean API in your page templates.
The clsx or cn Utility
For conditional classes, use clsx or the cn utility from shadcn:
// utils/cn.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
twMerge handles a common Tailwind footgun: class conflicts. If you apply px-4 and px-6 to the same element, which wins? Without twMerge, it depends on the order in the CSS file, which depends on the order in Tailwind's generated output — unpredictable. With twMerge, the last class wins:
<AppButton class="px-8">
<!-- AppButton has px-4, your override px-8 wins -->
</AppButton>
Dark Mode
Configure dark mode with the class strategy (manual toggle) or media strategy (follow OS preference):
// tailwind.config.ts
darkMode: 'class', // or 'media'
With class strategy, add the dark class to <html> when dark mode is active:
// composables/useColorMode.ts
export function useColorMode() {
const mode = ref<'light' | 'dark'>('light')
function toggle() {
mode.value = mode.value === 'light' ? 'dark' : 'light'
document.documentElement.classList.toggle('dark', mode.value === 'dark')
localStorage.setItem('color-mode', mode.value)
}
onMounted(() => {
const saved = localStorage.getItem('color-mode') as 'light' | 'dark' | null
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
mode.value = saved ?? (systemDark ? 'dark' : 'light')
document.documentElement.classList.toggle('dark', mode.value === 'dark')
})
return { mode, toggle }
}
Nuxt UI handles dark mode natively if you are using that component library — no manual implementation needed.
Typography Plugin
Install @tailwindcss/typography for article and documentation content:
npm install @tailwindcss/typography
Apply it to content rendered from Markdown:
<template>
<article class="prose prose-lg prose-gray dark:prose-invert max-w-none">
<ContentRenderer :value="post" />
</article>
</template>
The prose class applies sensible typographic defaults to all HTML elements inside the container. prose-invert switches to light-on-dark for dark mode. No manual styling of h1, h2, p, blockquote, and code tags — it just works.
Customize it in your Tailwind config to match your design:
typography: ({ theme }) => ({
gray: {
css: {
'--tw-prose-headings': theme('colors.gray.900'),
'--tw-prose-links': theme('colors.brand.600'),
'code::before': { content: 'none' },
'code::after': { content: 'none' },
},
},
}),
Nuxt UI: Pre-Built Components
If you want a full component library on top of Tailwind without building everything from scratch, @nuxt/ui is the official option:
npx nuxi module add ui
It provides buttons, modals, dropdowns, form inputs, tables, and more — all built on Tailwind and fully customizable through your Tailwind config. For projects where you need to move quickly without sacrificing design quality, it is a significant time saver.
Avoiding Common Mistakes
Do not use @apply extensively. It feels like a clean solution but defeats much of Tailwind's benefit. Reserve it for global base styles that apply to HTML elements directly.
Do not write utility classes in JavaScript strings outside of component files. Classes in JS strings are not scanned by Tailwind's content detection and will be purged in production.
Keep class lists readable. Long class strings on a single line are hard to review. Group them logically and break them across lines in complex cases.
Tailwind is a tool that rewards discipline. The setup takes minutes; the payoff comes from the consistent patterns you establish from the beginning.
Working on a Nuxt application and want help with your design system or component architecture? I am happy to review your setup. Book a call at calendly.com/jamesrossjr.