A compact search control with a clear action, Enter callback, and preserved keyboard focus.
"use client"
import * as React from "react"
import { SearchField } from "@/components/ballmac/search-field"
const items = [
"Design system",
"Motion presets",
"Content library",
"Project settings",
]
export default function SearchFieldDemo() {
const [query, setQuery] = React.useState("motion")
return (
<div className="w-full max-w-sm rounded-xl border bg-card p-4 shadow-sm">
<SearchField
value={query}
onValueChange={setQuery}
label="Search workspace"
placeholder="Search workspace"
/>
<div className="mt-3 flex flex-col gap-1">
{items
.filter((item) => item.toLowerCase().includes(query.toLowerCase()))
.map((item) => (
<div
key={item}
className="rounded-md px-2 py-2 text-sm hover:bg-accent"
>
{item}
</div>
))}
{!items.some((item) =>
item.toLowerCase().includes(query.toLowerCase()),
) && (
<p className="px-2 py-2 text-sm text-muted-foreground">
No matches yet
</p>
)}
</div>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/search-fieldInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/search-field.tsx// Ballmac UI: Search Field. https://ui.ballmac.com/components/search-field "use client" import * as React from "react" import { Search, X } from "lucide-react" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" type SearchFieldProps = Omit< React.ComponentProps<"input">, "type" | "value" | "defaultValue" | "onChange" > & { /** Controlled search query. */ value?: string /** Initial query when uncontrolled. */ defaultValue?: string /** Called whenever the query changes or clears. */ onValueChange?: (value: string) => void /** Called when Enter submits the query. */ onSearch?: (value: string) => void /** Accessible name of the search field. */ label?: string } function SearchField({ value, defaultValue = "", onValueChange, onSearch, label, className, disabled, onKeyDown, ...props }: SearchFieldProps) { const msg = useMessages() label ??= msg("search-field.label", "Search") const [internal, setInternal] = React.useState(defaultValue) const current = value ?? internal const inputRef = React.useRef<HTMLInputElement>(null) function commit(next: string) { if (value === undefined) setInternal(next) onValueChange?.(next) } return ( <div data-slot="search-field" className={cn( "flex h-9 w-full min-w-0 items-center gap-2 rounded-md border border-input bg-background px-3 shadow-xs transition-[border-color,box-shadow] duration-150 motion-reduce:transition-none focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50", className, )} > <Search aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" /> <input ref={inputRef} data-slot="search-field-input" type="search" aria-label={label} disabled={disabled} value={current} onChange={(event) => commit(event.currentTarget.value)} onKeyDown={(event) => { onKeyDown?.(event) if (!event.defaultPrevented && event.key === "Enter") onSearch?.(current) }} className="h-full min-w-0 flex-1 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground disabled:opacity-50 [&::-webkit-search-cancel-button]:hidden" {...props} /> {current && ( <button type="button" aria-label={msg("search-field.clearSearch", "Clear search")} disabled={disabled} onClick={() => { commit("") inputRef.current?.focus() }} className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50" > <X aria-hidden="true" className="size-4" /> </button> )} </div> ) } export { SearchField, type SearchFieldProps }Update the import paths to match your project setup.
Usage
import { SearchField } from "@/components/ballmac/search-field"The full example is in the Code tab above.
Examples
States and variants
Find a team member
import { SearchField } from "@/components/ballmac/search-field"
export default function SearchFieldStates() {
return (
<div className="w-full max-w-sm">
<p className="mb-2 text-sm font-medium">Find a team member</p>
<SearchField label="Find a team member" placeholder="Name or email" />
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
valueControlled search query. | string | — |
defaultValueInitial query when uncontrolled. | string | "" |
onValueChangeCalled whenever the query changes or clears. | (value: string) => void | — |
onSearchCalled when Enter submits the query. | (value: string) => void | — |
labelAccessible name of the search field. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Enter | Submits the current query |
| Tab / Space | Clears the query and returns focus |
Use with AI
A compact search control with a clear action, Enter callback, and preserved keyboard focus. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Search Field (@ballmac/search-field) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Filter a list or catalog
- Submit a query from a toolbar
Not for
- Use spotlight-search for site-wide command search
Registry JSON: https://ui.ballmac.com/r/search-field.json
Credits
Free to use in personal and commercial projects.
- npm
- lucide-react
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Input
A text input in three heights that match Button, plus InputGroup and InputGroupAddon for leading or trailing icons, units and domains.
Calendar
A date grid for one day, several days or a range, with month and year selects, week numbers, disabled rules and range-end styling, on React DayPicker.
Color Picker
A native color chooser paired with an editable hex field and a live swatch.
Combobox
A searchable single-choice select with groups, descriptions, keywords, clearable value, invalid state and hidden-input form submission.