概要
検索フィールドに値が入力されているときだけ × ボタンを表示し、クリック時に入力値と URL クエリの両方をリセットする。useRouter と useSearchParams を組み合わせて URL 同期を維持する。
インストール
# 追加インストールは不要
実装
クリアボタン付き検索フィールド
// components/ClearableSearchInput.tsx
"use client";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { useCallback, useRef, useState } from "react";
type Props = {
defaultValue?: string;
placeholder?: string;
};
export function ClearableSearchInput({
defaultValue = "",
placeholder = "キーワードで検索…",
}: Props) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [value, setValue] = useState(defaultValue);
const inputRef = useRef<HTMLInputElement>(null);
const submit = useCallback(
(q: string) => {
const params = new URLSearchParams(searchParams.toString());
if (q) {
params.set("q", q);
} else {
params.delete("q");
}
params.delete("page");
router.push(`${pathname}?${params.toString()}`);
},
[router, pathname, searchParams]
);
const handleClear = useCallback(() => {
setValue("");
submit("");
inputRef.current?.focus();
}, [submit]);
return (
<div className="relative">
<input
ref={inputRef}
type="search"
value={value}
placeholder={placeholder}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && submit(value)}
onBlur={() => submit(value)}
className="w-full rounded border border-gray-200 py-2 pl-3 pr-8 text-sm focus:outline-none focus:ring-1 focus:ring-blue-400"
/>
{value && (
<button
type="button"
onClick={handleClear}
aria-label="検索をクリア"
className="absolute inset-y-0 right-2 flex items-center text-gray-400 hover:text-gray-600"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
</button>
)}
</div>
);
}
Suspense でラップして使う
// app/samples/page.tsx(抜粋)
import { Suspense } from "react";
import { ClearableSearchInput } from "@/components/ClearableSearchInput";
export default async function SamplesPage({ searchParams }: Props) {
const params = await searchParams;
const q = typeof params.q === "string" ? params.q : "";
return (
<div>
<Suspense>
<ClearableSearchInput defaultValue={q} />
</Suspense>
{/* ...一覧 */}
</div>
);
}
type="search" のネイティブ × ボタンを非表示にする
ブラウザによっては type="search" に標準のクリアボタンが表示される。カスタムボタンと重複するため CSS で非表示にする。
/* app/globals.css */
input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
}
ポイント
value && (...)でボタンの表示を制御する。入力がない状態でクリアボタンを出さないことで、余分な UI ノイズを減らすhandleClearでステートのクリアと URL 更新を同時に行う。片方だけリセットすると表示と URL が乖離する- クリア後に
inputRef.current?.focus()でフォーカスを戻すと、ユーザーが次のキーワードをすぐ入力できる aria-label="検索をクリア"を必ずつける。アイコンのみのボタンはスクリーンリーダーが目的を読み取れないtype="search"のブラウザネイティブ × ボタンはカスタムボタンと重複するため CSS で非表示にする。Safari / Chrome / Edge で挙動が異なることがあるparams.delete("page")でページリセットを忘れない。クリア後にページ 2 以降が残ると、1 ページ目の結果が表示されない