Skip to main content
Engineering7 min readMarch 3, 2026

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.

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.


Keep Reading