PA01▸🧩 Compose · 互動組合▸搜索
Search Match
搜索媒合
難度 ★★★★★標籤 搜索 · Fuse.js · highlight · 狀態3 min read
模式簡述
搜尋框是「狀態最多」的 UI 之一:空、輸入中、載入、有結果、零結果、超量、錯誤。 每一個狀態都需要不同的視覺與文字提示。沒處理好就會讓使用者誤以為網頁壞了。
開始輸入以搜尋⋯
Claude Code Prompt
參考:PA01 search-match 技術:Fuse.js(模糊匹配)+ React 狀態 具體行為: - 輸入欄左側顯示 search icon - 空輸入:顯示「開始輸入以搜尋⋯」 - 輸入後 debounce 200ms 再查 - 命中字用 <mark> 標出(lab-accent 30% 透明度) - 結果超過 5 筆:只顯示前 5 + 「+ N 筆更多結果」 - 0 筆:顯示「找不到 OOO」+ 建議關鍵字 - 錯誤:顯示 retry 按鈕 限制: - 不要在每個 keystroke 都打 API,務必 debounce - 不要把 highlight 顏色做太花,會干擾閱讀 - mark 用 bg-lab-accent/30,不要用 fill
完整程式碼
'use client'
import { useMemo, useState } from 'react'
const DATA = ['Framer Motion', 'GSAP', 'Three.js', /* ... */]
function highlight(text: string, q: string) {
if (!q) return text
const i = text.toLowerCase().indexOf(q.toLowerCase())
if (i === -1) return text
return (
<>
{text.slice(0, i)}
<mark className="bg-lab-accent/30 rounded px-0.5">
{text.slice(i, i + q.length)}
</mark>
{text.slice(i + q.length)}
</>
)
}
export function SearchMatch() {
const [q, setQ] = useState('')
const results = useMemo(
() => (q ? DATA.filter(d => d.toLowerCase().includes(q.toLowerCase())) : []),
[q],
)
return (
<div>
<input value={q} onChange={e => setQ(e.target.value)} placeholder="搜尋⋯" />
{!q && <p>開始輸入以搜尋⋯</p>}
{q && results.length === 0 && <p>找不到「{q}」</p>}
<ul>
{results.slice(0, 5).map((r, i) => <li key={i}>{highlight(r, q)}</li>)}
</ul>
{results.length > 5 && <p>+ {results.length - 5} 筆更多結果</p>}
</div>
)
}何時用
- ✅ 站內搜尋(< 1000 筆資料,client side 即可)
- ✅ Command palette / 快速跳轉
- ✅ Select / Combobox 的篩選輸入
何時別用
- ❌ 資料 > 10000 筆,要走 server side + 分頁
- ❌ 需要全文檢索 / 同義詞,要走 Algolia / Meilisearch
- ❌ 不需要即時回饋的長文搜尋(書本內文,使用者願意等)
我踩過的坑
-
不做 debounce 直接查 API:使用者打 "ephraim" 7 個字會打 7 次 API。 debounce 200-300ms,請求量降一個數量級。
-
highlight 用整段背景色:早期把命中字用
bg-yellow-200一刀切, 結果跟暗色主題打架。改成bg-lab-accent/30+ 圓角,瞬間融入。 -
0 筆結果只寫「沒有」:使用者搞不清楚是查錯字還是真沒有。 解法:把搜尋詞 echo 出來 + 給建議關鍵字。
相關模式
- PA06 Filter Sidebar — 搜尋 + 篩選的組合