Tailwind CSS コンポーネントスタイリング
18.3.1. コンポーネント別スタイルパターン
コンポーネントスタイリングの基本原則は ユーティリティクラスの直接使用 です。
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>- 一般的なページコンポーネントではテンプレートにユーティリティクラスを直接記述します。
@applyは3回以上繰り返される再利用コンポーネントでのみ許可します。<style scoped>ブロックにカスタム CSS を記述することは最小限にします。
18.3.2. 動的クラスバインディング
Vue の :class ディレクティブを使用して条件付きクラスを適用します。
オブジェクト構文
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>配列構文
vue
<template>
<div :class="[baseClass, sizeClass, isDisabled ? 'opacity-50 cursor-not-allowed' : '']">
<slot />
</div>
</template>computed の活用
クラスの組み合わせが複雑な場合は computed で分離します。
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>- 静的クラスは
class属性に、動的クラスは:classに分離して記述します。 - 3つ以上の条件分岐が存在する場合は
computedに抽出しなければなりません。
18.3.3. バリエーションパターン
再利用コンポーネントは 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>- バリエーション値はユニオン型で制限し、任意の値の受け渡しを防止します。
- クラスマップは
Record型を使用して型安全性を確保します。 withDefaultsでデフォルト値を明示し、props が省略された場合でも安定的にレンダリングします。
18.3.4. ダークモード
Tailwind CSS の dark: プレフィックスを使用してダークモードを実装します。
tailwind.config.ts で darkMode を 'class' に設定します。
ts
// tailwind.config.ts
export default {
darkMode: 'class',
// ...
} satisfies Configmedia戦略の代わりにclass戦略を使用しなければなりません。class戦略はユーザーの選択を尊重します。
ダークモードクラスの適用
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">
タイトル
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
本文の内容です。
</p>
</div>
</template>テーマ切り替えの実装
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 ? 'ライトモード' : 'ダークモード' }}
</button>
</template>localStorageにユーザーの選択を保存し、リロード時にもテーマを維持します。<html>要素のdarkクラスをトグルして、ページ全体に適用します。