レビュー済み·難易度: 初級·更新: 2026-04-18

Tailwind CSS でフィルターチップ(アクティブフィルタ表示・削除)UI を実装する

適用中のフィルタ条件をチップ形式で横並びに表示し、個別削除ボタンと一括クリアボタンを持つフィルターチップコンポーネントを Tailwind CSS で実装する例。

nextjsstylingui-componenttailwindcss

対応バージョン

nextjs 15react 19tailwindcss 4

前提環境

Tailwind CSS の基本クラスと React コンポーネントの基本を理解していること

概要

適用中のフィルタ条件をチップ(Pill)形式で横並びに表示し、✕ボタンで個別削除・「すべてクリア」で一括削除できる UI コンポーネントを Tailwind CSS で実装する。Badge は静的なラベルだが、Filter Chip は削除インタラクションを持つ点が異なる。

インストール

# Tailwind CSS が既にセットアップ済みであれば追加インストールは不要
npm install tailwindcss @tailwindcss/postcss

実装

フィルターチップコンポーネント

// components/FilterChip.tsx
type Props = {
  label: string;
  value: string;
  onRemove: () => void;
};

export default function FilterChip({ label, value, onRemove }: Props) {
  return (
    <span className="inline-flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-sm font-medium text-blue-700 ring-1 ring-inset ring-blue-200">
      <span className="text-blue-400">{label}:</span>
      <span>{value}</span>
      <button
        type="button"
        onClick={onRemove}
        className="ml-0.5 flex h-4 w-4 items-center justify-center rounded-full text-blue-400 hover:bg-blue-100 hover:text-blue-600"
        aria-label={`${label} フィルタを削除`}
      >
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" className="h-3 w-3" fill="currentColor">
          <path d="M12.78 4.28a.75.75 0 0 0-1.06-1.06L8 6.94 4.28 3.22a.75.75 0 0 0-1.06 1.06L6.94 8l-3.72 3.72a.75.75 0 1 0 1.06 1.06L8 9.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L9.06 8l3.72-3.72z" />
        </svg>
      </button>
    </span>
  );
}

フィルターチップリスト

// components/FilterChipList.tsx
import FilterChip from "./FilterChip";

export type FilterEntry = {
  key: string;
  label: string;
  value: string;
};

type Props = {
  filters: FilterEntry[];
  onRemove: (key: string) => void;
  onClearAll: () => void;
};

export default function FilterChipList({ filters, onRemove, onClearAll }: Props) {
  if (filters.length === 0) return null;

  return (
    <div className="flex flex-wrap items-center gap-2">
      {filters.map((filter) => (
        <FilterChip
          key={filter.key}
          label={filter.label}
          value={filter.value}
          onRemove={() => onRemove(filter.key)}
        />
      ))}
      {filters.length > 1 && (
        <button
          type="button"
          onClick={onClearAll}
          className="text-sm text-gray-500 hover:text-gray-700 hover:underline"
        >
          すべてクリア
        </button>
      )}
    </div>
  );
}

URL フィルタと組み合わせた使用例

// components/SampleFilters.tsx
"use client";

import { Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import FilterChipList, { type FilterEntry } from "./FilterChipList";

const FILTER_LABELS: Record<string, string> = {
  q: "キーワード",
  category: "カテゴリ",
  difficulty: "難易度",
};

function SampleFiltersInner() {
  const router = useRouter();
  const searchParams = useSearchParams();

  const activeFilters: FilterEntry[] = Array.from(searchParams.entries())
    .filter(([key]) => key in FILTER_LABELS)
    .map(([key, value]) => ({
      key,
      label: FILTER_LABELS[key],
      value,
    }));

  function removeFilter(key: string) {
    const params = new URLSearchParams(searchParams.toString());
    params.delete(key);
    const qs = params.toString();
    router.push(qs ? `?${qs}` : window.location.pathname);
  }

  function clearAll() {
    router.push(window.location.pathname);
  }

  return (
    <FilterChipList
      filters={activeFilters}
      onRemove={removeFilter}
      onClearAll={clearAll}
    />
  );
}

export default function SampleFilters() {
  return (
    <Suspense fallback={null}>
      <SampleFiltersInner />
    </Suspense>
  );
}

バリエーション(サイズ・カラー)

// components/FilterChipVariants.tsx — スタイルバリエーション例

// グレー系(neutral)
<span className="inline-flex items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1 text-sm font-medium text-gray-700 ring-1 ring-inset ring-gray-200">
  {/* ... */}
</span>

// グリーン系(success)
<span className="inline-flex items-center gap-1.5 rounded-full bg-green-50 px-3 py-1 text-sm font-medium text-green-700 ring-1 ring-inset ring-green-200">
  {/* ... */}
</span>

// 小サイズ(sm)
<span className="inline-flex items-center gap-1 rounded-full bg-blue-50 px-2 py-0.5 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-200">
  {/* ... */}
</span>

ポイント

  • Filter Chip は削除ボタンを持つ点で Badge(静的ラベル)と区別する。aria-label で「○○ フィルタを削除」と読み上げさせてアクセシブルにする
  • ring-1 ring-inset でチップに内側のアウトラインを付ける。border より微妙な印象で、背景色と組み合わせたときに馴染みやすい
  • フィルタが 1 件のみのときは「すべてクリア」ボタンを非表示にしてノイズを減らす(filters.length > 1 の条件)
  • useSearchParams を使うコンポーネントは <Suspense> でラップする。Next.js App Router での要件
  • Array.from(searchParams.entries()) で全パラメータを走査し、FILTER_LABELS に定義したキーのみをフィルタとして扱う。ページネーションの page など内部パラメータがチップ表示されるのを防ぐ

注意点

tailwind-empty-state-ui は 0 件表示。tailwind-animation はアニメーション。react-multi-filter は複数条件フィルタ選択 UI。これはアクティブなフィルタ条件をチップ表示して削除できる UI パターンに特化。Badge コンポーネントと区別し、削除操作を持つインタラクティブチップとして扱う。

関連サンプル