Skip to content

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.
  • @apply is 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 class attribute, and dynamic classes in :class, keeping them separate.
  • When 3 or more conditional branches exist, they must be extracted into a computed property.

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 Record type to ensure type safety.
  • withDefaults must 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 class strategy must be used instead of the media strategy. The class strategy 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 localStorage to persist the theme across page refreshes.
  • The dark class is toggled on the <html> element to apply the theme across the entire page.

TIENIPIA QUALIFIED STANDARD