Search input
ProA search field. onValueChange hears every keystroke; onSearch hears the text once typing pauses (debounceMs), at once on Enter, and with "" at once when the field is cleared, by its Clear button or Escape. Focus stays in the field when Clear is pressed, since the button goes as the text does.
label names the field (a placeholder isn't a name). shortcut focuses it from anywhere on the page and is shown in the field and in aria-keyshortcuts. While busy a pulsing dot says it is searching. results is announced politely once a search settles: the first render after onSearch in which busy is false, so set busy (or the new results) in onSearch itself. null says nothing, for a count the app doesn't know.
For a button that opens a search surface use SearchTrigger; for the command palette, CommandTrigger.
Version 5.2.0· other versions
This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0 and 4.2.0: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
Loading the preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/search-input
Brings in: kbd, locale, motion, tokens, use-announcer, use-shortcut, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import * as React from "react"
import { SearchInput, SearchTrigger } from "@/components/candor/ui/search-input"
const runs = ["Deploy api to staging", "Deploy web to production", "Backfill orders", "Rotate signing keys", "Rebuild search index"]
export default function Example() {
const [query, setQuery] = React.useState("")
const [busy, setBusy] = React.useState(false)
const [results, setResults] = React.useState<number | null>(null)
const [opened, setOpened] = React.useState(0)
const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
const shown = runs.filter((r) => r.toLowerCase().includes(query.toLowerCase()))
return (
<div className="flex max-w-sm flex-col gap-3 rounded-xl border bg-card p-4">
<SearchInput
label="Search runs"
placeholder="Name or id"
shortcut="/"
busy={busy}
results={results}
onSearch={(q) => {
clearTimeout(timer.current)
if (!q) {
setQuery("")
setResults(null)
return
}
// A stand-in for a server search that takes a moment.
setBusy(true)
timer.current = setTimeout(() => {
setQuery(q)
setResults(runs.filter((r) => r.toLowerCase().includes(q.toLowerCase())).length)
setBusy(false)
}, 400)
}}
/>
<ul className="flex flex-col gap-1 text-sm">
{shown.map((r) => (
<li key={r}>{r}</li>
))}
</ul>
<SearchTrigger onOpen={() => setOpened((n) => n + 1)} shortcut="mod+k" />
<p className="text-xs text-muted-foreground">Search opened {opened} times.</p>
</div>
)
}<SearchInput>
| Prop | Type | Default | Description |
|---|---|---|---|
| labelrequired | string | — | The field's accessible name ("Search runs"). Required: the placeholder isn't a name. |
| value | string | undefined | — | The text, when the app holds it; pair it with `onValueChange`. Setting it to text other than what the person typed drops the search their typing was waiting on. |
| defaultValue | string | undefined | "" | The text it starts with, when the field holds it. |
| onValueChange | ((value: string) => void) | undefined | — | Called with the text on every keystroke, and with "" when it is cleared. |
| onSearch | ((value: string) => void) | undefined | — | Called with the text to search for: after `debounceMs` without typing, at once on Enter, and with "" at once when cleared. |
| debounceMs | number | undefined | 250 | How long typing must pause before `onSearch`, in milliseconds. 250 by default. |
| shortcut | string | undefined | — | A `useShortcut` combo ("mod+k", "/") that focuses the field from anywhere on the page, shown in it while it is empty. |
| busy | boolean | undefined | false | A search is running: a pulsing dot shows, and the field is `aria-busy`. |
| results | number | null | undefined | — | How many results the last search found, announced once it settles ("12 results", "No results"); `null` says nothing. |
| placeholder | string | undefined | — | Hint text inside the empty field ("Name, id or owner"). Not a name: `label` is. |
| className | string | undefined | — | Classes for the wrapper, merged with Candor's. |
<SearchTrigger>
A button drawn like a search field that opens a search surface (a dialog, a page, a panel), for a top bar or a sidebar. Its name is "Search" (or label), and shortcut, pressed anywhere on the page, opens it too: the keys are drawn on it and given in aria-keyshortcuts. For the command palette, CommandTrigger does the same.
| Prop | Type | Default | Description |
|---|---|---|---|
| onOpenrequired | () => void | — | Opens the search surface; called on a press and on the shortcut. |
| label | string | undefined | — | The button's words. "Search" by default. |
| shortcut | string | undefined | — | A `useShortcut` combo ("mod+k", "/") that opens it from anywhere on the page, drawn on the button. |
| className | string | undefined | — | Classes for the button, merged with Candor's. |
Strings
The fixed words, in English by default. Change any of them under searchInput in <LocaleProvider strings={{ … }}>; the table is exported as searchInputStrings.
| Key | English |
|---|---|
| clear | Clear search |
| searching | Searching |
| results | { one: "{count} result", other: "{count} results" } |
| noResults | No results |
| trigger | Search |
Motion
| Moment | Family | What it does |
|---|---|---|
| busy | live | While `busy` is set, a dot in the field pulses to say a search is running; it stops as soon as `busy` is cleared, and under reduced motion it is still. |
Set with --cd-motion-search-input-<moment>-* in your brand file; the families are in the design reference.