コア業務開発
次に、App.vue でコアのタスク管理ロジックを構築します。
全体のロジックは以下のとおりです:
- データの流れ:入力ボックスに内容を入力 ->
v-model更新newTaskTitle。 - アクショントリガー:Enter キーを押す -> 実行
addTask->tasks配列に 1 つの項目を追加。 - ビュー更新:
tasks変更 -> トリガーfilteredTasks自動再計算 -> Vue が再レンダリングliリスト。 - 状態遷移:フィルターボタンをクリック -> 変更
currentFilter値 -> 再トリガーfilteredTasks計算 -> ページに異なるカテゴリのタスクを表示。
基本レイアウトとリアクティブデータ
src/App.vue に次のように記述します:
サンプル
<script setup>
import { ref, computed } from 'vue';
// 1. タスクデータモデル
const tasks = ref([
{ id: '1', title: '体验 Tailwind v4 新特性', isCompleted: false },
{ id: '2', title: '掌握 Composition API', isCompleted: true }
]);
const newTaskTitle = ref('');
const currentFilter = ref('all');
// 2. コアメソッド
const addTask = () => {
const title = newTaskTitle.value.trim();
if (!title) return;
tasks.value.unshift({
id: crypto.randomUUID(),
title,
isCompleted: false
});
newTaskTitle.value = '';
};
const removeTask = (id) => {
tasks.value = tasks.value.filter(t => t.id !== id);
};
const toggleTask = (id) => {
const task = tasks.value.find(t => t.id === id);
if (task) task.isCompleted = !task.isCompleted;
};
// 3. 計算プロパティでフィルタリングを処理
const filteredTasks = computed(() => {
if (currentFilter.value === 'active') return tasks.value.filter(t => !t.isCompleted);
if (currentFilter.value === 'completed') return tasks.value.filter(t => t.isCompleted);
return tasks.value;
});
</script>
<template>
<div class="min-h-screen py-12 px-4">
<div class="max-w-md mx-auto bg-white rounded-3xl shadow-xl shadow-slate-200 border border-slate-100 overflow-hidden">
<header class="bg-linear-to-br from-blue-600 to-indigo-700 p-8 text-white">
<h1 class="text-3xl font-black tracking-tight">TaskHub</h1>
<p class="text-blue-100/80 text-sm">Vue 3 + Tailwind v4 実践</p>
</header>
<main class="p-6">
<div class="flex gap-2 mb-8">
<input
v-model="newTaskTitle"
@keyup.enter="addTask"
placeholder="今日は何を完了しますか?"
class="flex-1 bg-slate-50 border-none rounded-2xl px-4 py-3 focus:ring-2 focus:ring-brand/50 outline-none transition-all"
/>
<button @click="addTask" class="bg-brand text-white px-6 rounded-2xl font-bold hover:scale-105 active:scale-95 transition-all">
+
</button>
</div>
<div class="flex p-1 bg-slate-100 rounded-xl mb-6">
<button
v-for="f in ['all', 'active', 'completed']"
:key="f"
@click="currentFilter = f"
:class="[
'flex-1 py-1.5 text-xs font-bold rounded-lg transition-all capitalize',
currentFilter === f ? 'bg-white text-brand shadow-sm' : 'text-slate-500'
]"
>
{{ f }}
</button>
</div>
<ul class="space-y-3">
<TransitionGroup name="list">
<li
v-for="task in filteredTasks"
:key="task.id"
class="group flex items-center justify-between p-4 bg-slate-50 rounded-2xl border border-transparent hover:border-slate-200 hover:bg-white transition-all"
>
<div class="flex items-center gap-3">
<input
type="checkbox"
:checked="task.isCompleted"
@change="toggleTask(task.id)"
class="w-5 h-5 accent-brand cursor-pointer"
/>
<span :class="['text-slate-700 font-medium', task.isCompleted ? 'line-through text-slate-400 opacity-50' : '']">
{{ task.title }}
</span>
</div>
<button @click="removeTask(task.id)" class="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-500 transition-all p-1">
✕
</button>
</li>
</TransitionGroup>
</ul>
<div v-if="filteredTasks.length === 0" class="text-center py-12 text-slate-400 text-sm">
関連するタスクはありません...
</div>
</main>
</div>
</div>
</template>
<style scoped>
/* リストトランジションアニメーション */
.list-enter-active, .list-leave-active { transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28); }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); }
</style>
import { ref, computed } from 'vue';
// 1. タスクデータモデル
const tasks = ref([
{ id: '1', title: '体验 Tailwind v4 新特性', isCompleted: false },
{ id: '2', title: '掌握 Composition API', isCompleted: true }
]);
const newTaskTitle = ref('');
const currentFilter = ref('all');
// 2. コアメソッド
const addTask = () => {
const title = newTaskTitle.value.trim();
if (!title) return;
tasks.value.unshift({
id: crypto.randomUUID(),
title,
isCompleted: false
});
newTaskTitle.value = '';
};
const removeTask = (id) => {
tasks.value = tasks.value.filter(t => t.id !== id);
};
const toggleTask = (id) => {
const task = tasks.value.find(t => t.id === id);
if (task) task.isCompleted = !task.isCompleted;
};
// 3. 計算プロパティでフィルタリングを処理
const filteredTasks = computed(() => {
if (currentFilter.value === 'active') return tasks.value.filter(t => !t.isCompleted);
if (currentFilter.value === 'completed') return tasks.value.filter(t => t.isCompleted);
return tasks.value;
});
</script>
<template>
<div class="min-h-screen py-12 px-4">
<div class="max-w-md mx-auto bg-white rounded-3xl shadow-xl shadow-slate-200 border border-slate-100 overflow-hidden">
<header class="bg-linear-to-br from-blue-600 to-indigo-700 p-8 text-white">
<h1 class="text-3xl font-black tracking-tight">TaskHub</h1>
<p class="text-blue-100/80 text-sm">Vue 3 + Tailwind v4 実践</p>
</header>
<main class="p-6">
<div class="flex gap-2 mb-8">
<input
v-model="newTaskTitle"
@keyup.enter="addTask"
placeholder="今日は何を完了しますか?"
class="flex-1 bg-slate-50 border-none rounded-2xl px-4 py-3 focus:ring-2 focus:ring-brand/50 outline-none transition-all"
/>
<button @click="addTask" class="bg-brand text-white px-6 rounded-2xl font-bold hover:scale-105 active:scale-95 transition-all">
+
</button>
</div>
<div class="flex p-1 bg-slate-100 rounded-xl mb-6">
<button
v-for="f in ['all', 'active', 'completed']"
:key="f"
@click="currentFilter = f"
:class="[
'flex-1 py-1.5 text-xs font-bold rounded-lg transition-all capitalize',
currentFilter === f ? 'bg-white text-brand shadow-sm' : 'text-slate-500'
]"
>
{{ f }}
</button>
</div>
<ul class="space-y-3">
<TransitionGroup name="list">
<li
v-for="task in filteredTasks"
:key="task.id"
class="group flex items-center justify-between p-4 bg-slate-50 rounded-2xl border border-transparent hover:border-slate-200 hover:bg-white transition-all"
>
<div class="flex items-center gap-3">
<input
type="checkbox"
:checked="task.isCompleted"
@change="toggleTask(task.id)"
class="w-5 h-5 accent-brand cursor-pointer"
/>
<span :class="['text-slate-700 font-medium', task.isCompleted ? 'line-through text-slate-400 opacity-50' : '']">
{{ task.title }}
</span>
</div>
<button @click="removeTask(task.id)" class="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-500 transition-all p-1">
✕
</button>
</li>
</TransitionGroup>
</ul>
<div v-if="filteredTasks.length === 0" class="text-center py-12 text-slate-400 text-sm">
関連するタスクはありません...
</div>
</main>
</div>
</div>
</template>
<style scoped>
/* リストトランジションアニメーション */
.list-enter-active, .list-leave-active { transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28); }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); }
</style>
実行後の画面は以下のようになります:

コア知識ポイント
1. コンポジション API (Composition API)
上記のコードは、Vue 3 の最も中核となるロジックの構成方法を示しています:
ref(リアクティブの基礎):tasks、newTaskTitleなどはリアクティブ参照です。<script>内では、それらを変更するには、次のものを使う必要があります:.value(例:tasks.value = ...)、ただし、<template>内では変数名を直接記述します。Vue はこれらの値の変化を自動的に追跡して UI を更新します。computed(計算プロパティ):filteredTasksは非常に典型的な応用例です。これはtasksとcurrentFilterに基づいて派生しています。- 利点:それはリアクティブで、かつキャッシュ性を備えたものです。。もし
tasks変わっていない場合、ページを何度レンダリングしても、フィルタリングロジックを繰り返し実行しません。
2. リストレンダリングと Key 値
<li v-for="task in filteredTasks" :key="task.id">
:keyの重要性:Vue では、リストのループには必ずkeyを提供する必要があります。ここではcrypto.randomUUID()で生成された一意の ID です。これにより、リストの順序が変わったり項目が削除されたりした場合でも、Vue は特定の DOM ノードを正確に識別でき、レンダリングの混乱を避けられます(特にアニメーションや入力状態を持つリストの場合)。
3. イベント処理と修飾子
@keyup.enter:これはキー修飾子。これにより、addTask関数はユーザーが Enter キーを押したときのみ実行され、入力体験が大幅に向上します。- イベントの引数渡し:
removeTask(task.id)テンプレート内で現在のループ項目のデータを関数に渡す方法を示しています。
4. 双方向バインディング (v-model)
v-model="newTaskTitle"入力ボックスと変数の同期を実現します。- 原理:これは実際には
:valueと@inputの糖衣構文(シンタックスシュガー)です。
5. アニメーションシステム (TransitionGroup)
- これは Vue 組み込みのコンポーネントで、リスト(
v-for)の追加・削除を処理するために特化しています。 - 要素が出入りするときに CSS クラスを自動的に切り替えます(例:
.list-enter-from)。コード下部のstyleと組み合わせることで、リスト項目が滑らかにスライドイン/スライドアウトする効果を実現します。
Tailwind v4 のスタイル特性の解説
Tailwind v4 は v3 に比べて、よりネイティブ CSS の能力と簡潔さ。
1. 新しいグラデーション構文
bg-linear-to-br:v4 では、グラデーションにbg-gradient-to-brのような冗長な書き方は不要で、直接linear-to-方位よりネイティブ CSS に近いlinear-gradient。
2. 透明度の処理
text-blue-100/80:/80は 80% の透明度を表します。v4 の色の透明度処理は非常に直感的で、複雑な色設定をあらかじめ用意する必要はありません。
3. テーマ変数の参照
bg-brand:CSS の@theme内で定義しています。--color-brand。v4 では、CSS 変数を定義するだけで、Tailwind は対応するユーティリティクラスを自動生成します(例:bg-brand,text-brand,border-brand)。JS 設定ファイルをいじる必要はありません。
4. 強力なインタラクティブ擬似クラス
groupと `group-hover:opacity-100`:- に
liに設定しますgroup。 - 削除ボタンに設定します
group-hover:opacity-100。 - 効果:「この行にマウスがホバーしたときだけ削除ボタンが表示される」を実現します。このような親子状態の連動JS コードを 1 行も書く必要はありません。
active:scale-95:CSS 擬似クラスを利用して、クリック時のボタン拡大縮小フィードバックをすばやく実現します。
5. モダンなボーダーとリングシャドウ
focus:ring-2 focus:ring-brand/50:入力ボックスがフォーカスされたとき、ring属性を使用して外側に発光するボーダーを作成し、brand/50光のにじみが透明感を持つことを保証します(無機質な単色ではなく)。