SearchBox
Modal search widget for global application search
SearchBox
The SearchBox widget provides a modal search interface for global application search. It's typically triggered by a keyboard shortcut (Cmd+K) and allows users to quickly find and navigate to content, commands, or settings.
Type Signature
interface SearchBoxProps {
/** Search query */
query?: string
/** Query change handler */
onQueryChange?: (query: string) => void
/** Search results */
results?: SearchResult[]
/** Result selection handler */
onSelect?: (result: SearchResult) => void
/** Placeholder text */
placeholder?: string
/** Show recent searches */
showRecent?: boolean
/** Recent searches */
recentSearches?: string[]
/** Search categories */
categories?: string[]
/** Enable keyboard shortcuts */
shortcuts?: boolean
/** Loading state */
loading?: boolean
}
interface SearchResult {
id: string
title: string
description?: string
category?: string
icon?: string
url?: string
metadata?: any
}Usage
<SearchBox
placeholder="Search..."
onQueryChange={(query) => setQuery(query)}
results={searchResults}
onSelect={(result) => {
navigate(result.url)
}}
shortcuts={true}
showRecent={true}
/>Props
query
Current search query value.
<SearchBox
query={query}
onQueryChange={setQuery}
/>onQueryChange
Callback when the search query changes.
<SearchBox
onQueryChange={(query) => {
setQuery(query)
performSearch(query)
}}
/>results
Array of search results to display.
<SearchBox
results={[
{
id: '1',
title: 'User Settings',
description: 'Manage your account settings',
category: 'Settings',
icon: 'Settings',
url: '/settings',
},
{
id: '2',
title: 'API Keys',
description: 'View and manage API keys',
category: 'Developer',
icon: 'Key',
url: '/keys',
},
]}
/>onSelect
Callback when a search result is selected.
<SearchBox
results={results}
onSelect={(result) => {
// Navigate to result
navigate(result.url)
// Or execute command
result.action?.()
}}
/>placeholder
Placeholder text in the search input. Defaults to "Search...".
<SearchBox placeholder="Search or type a command..." />showRecent
Display recent searches when query is empty. Defaults to true.
<SearchBox
showRecent={true}
recentSearches={['API Keys', 'Settings', 'Dashboard']}
/>shortcuts
Enable keyboard shortcuts display. Defaults to true.
<SearchBox shortcuts={true} />loading
Show loading state while searching.
<SearchBox
loading={isSearching}
results={results}
/>Widget Context Usage
SearchBox is typically used as a modal overlay:
Modal Context
Full-screen modal search:
function App() {
const [searchOpen, setSearchOpen] = useState(false)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
setSearchOpen(true)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [])
return (
<>
<Shell>
<DashboardView />
</Shell>
<Dialog open={searchOpen} onClose={() => setSearchOpen(false)}>
<SearchBox
onSelect={(result) => {
navigate(result.url)
setSearchOpen(false)
}}
/>
</Dialog>
</>
)
}Command Palette Mode
SearchBox can also function as a command palette:
<SearchBox
placeholder="Type a command or search..."
results={[
// Navigation items
{
id: 'nav-dashboard',
title: 'Dashboard',
category: 'Navigate',
icon: 'LayoutDashboard',
url: '/',
},
// Commands
{
id: 'cmd-new-key',
title: 'Create New API Key',
category: 'Commands',
icon: 'Plus',
action: () => createApiKey(),
},
{
id: 'cmd-export',
title: 'Export Data',
category: 'Commands',
icon: 'Download',
action: () => exportData(),
},
]}
/>Search Categories
Organize results by category:
<SearchBox
categories={['Pages', 'Settings', 'Commands', 'Help']}
results={[
// Pages
{
id: '1',
title: 'Dashboard',
category: 'Pages',
url: '/',
},
{
id: '2',
title: 'API Keys',
category: 'Pages',
url: '/keys',
},
// Settings
{
id: '3',
title: 'Account Settings',
category: 'Settings',
url: '/settings/account',
},
{
id: '4',
title: 'Security Settings',
category: 'Settings',
url: '/settings/security',
},
// Commands
{
id: '5',
title: 'Create New Key',
category: 'Commands',
action: () => createKey(),
},
]}
/>Common Patterns
Global Navigation Search
function GlobalSearch() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
useEffect(() => {
if (query.length === 0) {
setResults([])
return
}
const searchResults = [
// Search pages
...pages.filter((p) =>
p.title.toLowerCase().includes(query.toLowerCase())
),
// Search settings
...settings.filter((s) =>
s.title.toLowerCase().includes(query.toLowerCase())
),
// Search docs
...docs.filter((d) =>
d.title.toLowerCase().includes(query.toLowerCase())
),
]
setResults(searchResults)
}, [query])
return (
<SearchBox
query={query}
onQueryChange={setQuery}
results={results}
placeholder="Search pages, settings, and docs..."
/>
)
}API Search
function APISearch() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)
const performSearch = useMemo(
() =>
debounce(async (q: string) => {
if (q.length < 2) return
setLoading(true)
try {
const data = await fetch(`/api/search?q=${q}`).then((r) => r.json())
setResults(data.results)
} finally {
setLoading(false)
}
}, 300),
[]
)
useEffect(() => {
performSearch(query)
}, [query, performSearch])
return (
<SearchBox
query={query}
onQueryChange={setQuery}
results={results}
loading={loading}
placeholder="Search..."
/>
)
}Multi-Collection Search
<SearchBox
results={[
// Users
{
id: 'user-1',
title: 'John Doe',
description: 'john@example.com',
category: 'Users',
icon: 'User',
url: '/users/1',
},
// Projects
{
id: 'project-1',
title: 'Acme Project',
description: 'Internal tool development',
category: 'Projects',
icon: 'Folder',
url: '/projects/1',
},
// Documents
{
id: 'doc-1',
title: 'API Documentation',
description: 'REST API reference',
category: 'Documents',
icon: 'FileText',
url: '/docs/api',
},
]}
/>Command Palette
<SearchBox
placeholder="Type a command..."
results={[
{
id: 'new-project',
title: 'New Project',
description: 'Create a new project',
category: 'Create',
icon: 'Plus',
action: () => createProject(),
},
{
id: 'invite-user',
title: 'Invite Team Member',
description: 'Send an invitation',
category: 'Team',
icon: 'UserPlus',
action: () => inviteUser(),
},
{
id: 'export-data',
title: 'Export Data',
description: 'Download your data',
category: 'Data',
icon: 'Download',
action: () => exportData(),
},
]}
/>Recent Searches
Show recent searches when query is empty:
function SearchWithRecent() {
const [query, setQuery] = useState('')
const [recentSearches, setRecentSearches] = useState([
'API Keys',
'Settings',
'Dashboard',
])
const addRecentSearch = (q: string) => {
setRecentSearches((prev) => {
const filtered = prev.filter((s) => s !== q)
return [q, ...filtered].slice(0, 5)
})
}
return (
<SearchBox
query={query}
onQueryChange={setQuery}
showRecent={query.length === 0}
recentSearches={recentSearches}
onSelect={(result) => {
addRecentSearch(query)
navigate(result.url)
}}
/>
)
}Keyboard Navigation
SearchBox supports full keyboard navigation:
| Key | Action |
|---|---|
Cmd/Ctrl + K | Open search |
Escape | Close search |
↑ / ↓ | Navigate results |
Enter | Select result |
Tab | Next category |
Shift + Tab | Previous category |
<SearchBox
shortcuts={true}
onKeyDown={(e) => {
if (e.key === 'Enter') {
selectResult(selectedResult)
} else if (e.key === 'Escape') {
closeSearch()
}
}}
/>Search Highlighting
Highlight matching text in results:
function HighlightedSearchBox() {
const [query, setQuery] = useState('')
const highlightMatch = (text: string, query: string) => {
if (!query) return text
const regex = new RegExp(`(${query})`, 'gi')
return text.replace(regex, '<mark>$1</mark>')
}
return (
<SearchBox
query={query}
onQueryChange={setQuery}
renderResult={(result) => (
<div>
<div
dangerouslySetInnerHTML={{
__html: highlightMatch(result.title, query),
}}
/>
<div className="text-sm text-muted-foreground">
{result.description}
</div>
</div>
)}
/>
)
}Empty States
Handle different empty states:
<SearchBox
query={query}
results={results}
emptyState={
query.length === 0 ? (
<div className="text-center py-8">
<p className="text-muted-foreground">
Start typing to search...
</p>
</div>
) : results.length === 0 ? (
<div className="text-center py-8">
<p className="text-muted-foreground">
No results found for "{query}"
</p>
</div>
) : null
}
/>Search Scopes
Allow scoping search to specific areas:
function ScopedSearch() {
const [scope, setScope] = useState('all')
const [query, setQuery] = useState('')
const scopes = {
all: 'All',
pages: 'Pages',
settings: 'Settings',
docs: 'Documentation',
}
return (
<>
<Select value={scope} onValueChange={setScope}>
{Object.entries(scopes).map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</Select>
<SearchBox
query={query}
onQueryChange={setQuery}
results={results.filter(
(r) => scope === 'all' || r.category === scope
)}
placeholder={`Search ${scopes[scope].toLowerCase()}...`}
/>
</>
)
}Integration with Payload CMS
Search across Payload collections:
function PayloadSearch() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
useEffect(() => {
if (query.length < 2) return
const searchCollections = async () => {
const [posts, pages, users] = await Promise.all([
payload.find({
collection: 'posts',
where: { title: { contains: query } },
}),
payload.find({
collection: 'pages',
where: { title: { contains: query } },
}),
payload.find({
collection: 'users',
where: { name: { contains: query } },
}),
])
setResults([
...posts.docs.map((p) => ({
id: p.id,
title: p.title,
category: 'Posts',
url: `/posts/${p.id}`,
})),
...pages.docs.map((p) => ({
id: p.id,
title: p.title,
category: 'Pages',
url: `/pages/${p.id}`,
})),
...users.docs.map((u) => ({
id: u.id,
title: u.name,
category: 'Users',
url: `/users/${u.id}`,
})),
])
}
searchCollections()
}, [query])
return (
<SearchBox
query={query}
onQueryChange={setQuery}
results={results}
/>
)
}Accessibility
SearchBox implements accessibility best practices:
- ARIA labels - Descriptive labels for screen readers
- Keyboard navigation - Full keyboard support
- Focus management - Proper focus handling
- Live regions - Result updates announced to screen readers
<div role="combobox" aria-expanded="true" aria-haspopup="listbox">
<input
type="search"
role="searchbox"
aria-label="Search"
aria-controls="search-results"
/>
<ul id="search-results" role="listbox">
<li role="option" aria-selected="true">
Dashboard
</li>
<li role="option">
Settings
</li>
</ul>
</div>Best Practices
Debounce API Searches
Avoid excessive API calls:
const performSearch = useMemo(
() =>
debounce(async (query: string) => {
const results = await searchAPI(query)
setResults(results)
}, 300),
[]
)Show Relevant Results First
Prioritize results by relevance:
const sortResults = (results: SearchResult[], query: string) => {
return results.sort((a, b) => {
// Exact matches first
const aExact = a.title.toLowerCase() === query.toLowerCase()
const bExact = b.title.toLowerCase() === query.toLowerCase()
if (aExact && !bExact) return -1
if (!aExact && bExact) return 1
// Starts with query
const aStarts = a.title.toLowerCase().startsWith(query.toLowerCase())
const bStarts = b.title.toLowerCase().startsWith(query.toLowerCase())
if (aStarts && !bStarts) return -1
if (!aStarts && bStarts) return 1
// Alphabetical
return a.title.localeCompare(b.title)
})
}Limit Results
Show a reasonable number of results:
<SearchBox
results={results.slice(0, 10)}
/>Provide Clear Categories
Group results for easier scanning:
<SearchBox
results={results}
categories={['Pages', 'Settings', 'Commands']}
/>Related Components
- DetailPanel - Detail side panel component
- ActivityPanel - Activity feed component
- ChatBox - Slideover chat widget
- CommandPalette - Keyboard commands widget