Tailwind CSS Component Styling
18.3.1. Per-Component Style Patterns
The fundamental principle of component styling is direct use of utility classes.
vue
<template>
<div class="flex items-center gap-4 rounded-lg border border-gray-200 p-4">
<img
:src="user.avatar"
:alt="user.name"
class="h-12 w-12 rounded-full object-cover"
/>
<div>
<p class="text-sm font-semibold text-gray-900">{{ user.name }}</p>
<p class="text-xs text-gray-500">{{ user.email }}</p>
</div>
</div>
</template>- In general page components, utility classes must be written directly in the template.
@applyis permitted only in reusable components where the same combination is repeated 3 or more times.- Writing custom CSS in
<style scoped>blocks should be minimized.
18.3.2. Dynamic Class Binding
Vue's :class directive is used to apply conditional classes.
Object Syntax
vue
<template>
<button
:class="{
'bg-brand-600 text-white': isActive,
'bg-gray-100 text-gray-700': !isActive,
}"
class="rounded-md px-4 py-2 text-sm font-medium"
@click="toggle"
>
{{ label }}
</button>
</template>Array Syntax
vue
<template>
<div :class="[baseClass, sizeClass, isDisabled ? 'opacity-50 cursor-not-allowed' : '']">
<slot />
</div>
</template>Using computed
When class combinations are complex, they should be extracted into a computed property.
vue
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
status: 'success' | 'warning' | 'error'
}>()
const statusClass = computed(() => {
const classMap: Record<string, string> = {
success: 'bg-green-100 text-green-800 border-green-200',
warning: 'bg-yellow-100 text-yellow-800 border-yellow-200',
error: 'bg-red-100 text-red-800 border-red-200',
}
return classMap[props.status]
})
</script>
<template>
<span :class="statusClass" class="inline-flex rounded-full border px-3 py-1 text-xs font-medium">
<slot />
</span>
</template>- Static classes must be placed in the
classattribute, and dynamic classes in:class, keeping them separate. - When 3 or more conditional branches exist, they must be extracted into a
computedproperty.
18.3.3. Variant Patterns
Reusable components provide style variants through props.
vue
<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
size?: 'sm' | 'md' | 'lg'
variant?: 'primary' | 'secondary' | 'outline'
}>(),
{
size: 'md',
variant: 'primary',
},
)
const sizeClass = computed(() => {
const map: Record<string, string> = {
sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
}
return map[props.size]
})
const variantClass = computed(() => {
const map: Record<string, string> = {
primary: 'bg-brand-600 text-white hover:bg-brand-700',
secondary: 'bg-gray-600 text-white hover:bg-gray-700',
outline: 'border border-brand-600 text-brand-600 hover:bg-brand-50',
}
return map[props.variant]
})
</script>
<template>
<button
:class="[sizeClass, variantClass]"
class="inline-flex items-center justify-center rounded-md font-medium transition-colors
focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2"
>
<slot />
</button>
</template>- Variant values must be restricted to union types to prevent arbitrary value passing.
- Class maps must use the
Recordtype to ensure type safety. withDefaultsmust be used to specify default values so that components render reliably even when props are omitted.
18.3.4. Dark Mode
Dark mode is implemented using Tailwind CSS's dark: prefix.
Set darkMode to 'class' in tailwind.config.ts.
ts
// tailwind.config.ts
export default {
darkMode: 'class',
// ...
} satisfies Config- The
classstrategy must be used instead of themediastrategy. Theclassstrategy respects user preference.
Applying Dark Mode Classes
vue
<template>
<div class="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<h2 class="text-lg font-bold text-gray-800 dark:text-gray-200">
Title
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
Body content.
</p>
</div>
</template>Theme Toggle Implementation
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const isDark = ref(false)
function toggleTheme() {
isDark.value = !isDark.value
document.documentElement.classList.toggle('dark', isDark.value)
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
}
onMounted(() => {
const saved = localStorage.getItem('theme')
isDark.value = saved === 'dark'
document.documentElement.classList.toggle('dark', isDark.value)
})
</script>
<template>
<button
class="rounded-md p-2 text-gray-600 hover:bg-gray-100
dark:text-gray-400 dark:hover:bg-gray-800"
@click="toggleTheme"
>
{{ isDark ? 'Light Mode' : 'Dark Mode' }}
</button>
</template>- The user's selection is stored in
localStorageto persist the theme across page refreshes. - The
darkclass is toggled on the<html>element to apply the theme across the entire page.