PE01▸⌨️ Input · 進階輸入▸選擇
Combobox
組合輸入框
難度 ★★★★★標籤 combobox · autocomplete · 鍵盤3 min read
模式簡述
Combobox:輸入框 + 下拉選項 + 即時篩選。autocomplete、tag 選擇、language picker 都會用。
Claude Code Prompt
參考:PE01 combobox 組件:Combobox.tsx Combobox:輸入框 + 下拉選項 + 即時篩選。autocomplete、tag 選擇、language picker 都會用。 限制: - 沒做鍵盤導航 - 沒高亮命中字 - 清空輸入沒清掉選擇
完整程式碼
"use client";
import { useEffect, useState } from "react";
import { ChevronDown, Check } from "lucide-react";
type State = "closed" | "open" | "filtered" | "selected";
const OPTIONS = ["React", "Vue", "Svelte", "Solid", "Angular", "Qwik"];
export function Combobox({ state = "open" }: { state?: State }) {
const [open, setOpen] = useState(state !== "closed" && state !== "selected");
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<string | null>(null);
useEffect(() => {
setOpen(state === "open" || state === "filtered");
setQuery(state === "filtered" ? "s" : "");
setSelected(state === "selected" ? "React" : null);
}, [state]);
const filtered = query
? OPTIONS.filter((o) => o.toLowerCase().includes(query.toLowerCase()))
: OPTIONS;
return (
<div className="w-full max-w-xs">
<div className="relative">
<button
onClick={() => setOpen((o) => !o)}
className="flex w-full items-center justify-between rounded-md border border-border bg-background/50 px-3 py-2 text-xs hover:border-lab-accent"
>
<span className={selected ? "text-foreground" : "text-muted-foreground"}>
{selected ?? "選擇框架⋯"}
</span>
<ChevronDown className="h-3 w-3" />
</button>
{open && (
<div className="absolute inset-x-0 top-full z-10 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-xl animate-in fade-in zoom-in-95 duration-100">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜尋⋯"
autoFocus
className="w-full border-b border-border bg-transparent px-3 py-2 text-xs outline-none placeholder:text-muted-foreground"
/>
<ul className="max-h-40 overflow-y-auto p-1">
{filtered.length === 0 ? (
<li className="px-2 py-2 text-center font-mono text-[11px] text-muted-foreground">
查無
</li>
) : (
filtered.map((o) => (
<li key={o}>
<button
onClick={() => {
setSelected(o);
setOpen(false);
setQuery("");
}}
className="flex w-full items-center justify-between rounded px-2 py-1.5 text-left text-xs hover:bg-lab-accent/10"
>
{o}
{selected === o && <Check className="h-3 w-3 text-lab-accent" />}
</button>
</li>
))
)}
</ul>
</div>
)}
</div>
</div>
);
}何時用
- ✅ 選項超過 10 個的選擇情境
- ✅ 需要使用者也能輸入新值(creatable)
- ✅ 需要鍵盤導航的進階表單
何時別用
- ❌ 選項少於 5 個(用 radio 或 select)
- ❌ 需要多選(用 multi-select)
- ❌ 選項是日期、數字等特殊類型
我踩過的坑
-
沒做鍵盤導航:ArrowDown 切下一個、Enter 選擇、Esc 關閉。少一個都不及格。
-
沒高亮命中字:使用者輸入 ‘re’ 看不出哪幾個含 re。要 highlight match。
-
清空輸入沒清掉選擇:使用者刪到空白但 selected 還在,狀態錯亂。
相關模式
- PB07
- PE03