Frontend Lab
← 回到所有模式
PE01⌨️ Input · 進階輸入選擇

Combobox

組合輸入框

難度 ★★★★標籤 combobox · autocomplete · 鍵盤3 min read

模式簡述

Combobox:輸入框 + 下拉選項 + 即時篩選。autocomplete、tag 選擇、language picker 都會用。

關閉|未開啟

Claude Code Prompt

💬 Claude Code Prompt
參考:PE01 combobox
組件:Combobox.tsx

Combobox:輸入框 + 下拉選項 + 即時篩選。autocomplete、tag 選擇、language picker 都會用。

限制:
- 沒做鍵盤導航
- 沒高亮命中字
- 清空輸入沒清掉選擇

完整程式碼

Combobox.tsx
"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)
  • ❌ 選項是日期、數字等特殊類型

我踩過的坑

  1. 沒做鍵盤導航:ArrowDown 切下一個、Enter 選擇、Esc 關閉。少一個都不及格。

  2. 沒高亮命中字:使用者輸入 ‘re’ 看不出哪幾個含 re。要 highlight match。

  3. 清空輸入沒清掉選擇:使用者刪到空白但 selected 還在,狀態錯亂。

相關模式

  • PB07
  • PE03

搜尋

按 ⌘K 隨時開啟