Expandable Search
A search that rests as an icon button and grows into a full field on click. One box widens under the magnifier, so the glyph never moves and the open field is the DS Input, pixel for pixel.
Installation#
Usage#
import { ExpandableSearch } from "@/components/ui/expandable-search"
export function Example() {
return (
<ExpandableSearch
placeholder="Search files"
className="w-64" // the OPEN width
inputProps={{ onChange: (e) => setQuery(e.target.value) }}
/>
)
}How it opens and closes#
A click opens it and puts the caret in the field. Escape steps back one layer at a time: the first press clears the query, the second folds the search away and hands focus back to the button. Clicking elsewhere folds an empty search, but one that holds a query stays open, so the text filtering the page never disappears behind an icon. The width transition names its properties and switches off under reduced motion.
Sizes#
The Input's own axis: sm 32px, md 36px, lg 40px. At rest it is a square of that height, the same size as a ghost icon Button beside it; unset, it follows the container's imposed size or the density.
With a shortcut#
Trailing content rides inside the open field, like a Kbd hint. Control open when something outside the component opens it: press / anywhere on this page.
States#
At rest, open with a query, and disabled. It is an InputRoot underneath, so hasError, a surrounding Field and the focus ring all behave exactly as they do on an Input.
In a navbar#
NavbarSearch collapsible is this component tuned for the bar: 36px and rounded-md at rest, the hamburger's chip, so it reads as one of the bar's icons until someone searches.
API reference#
ExpandableSearch
An InputRoot whose leading magnifier is the button that opens it. Forwards InputRoot props (hasError, disabled, density…).
open/defaultOpen/onOpenChange: the expanded state. Focus moves only for the component's own button, never fordefaultOpenor a controlled change.size:"sm","md"or"lg". The closed square and the open field's height.className: merged onto the box; a width class (w-80) sets the OPEN width, defaultw-64. The closed width is pinned by the size, so it never fights yours.placeholder: default"Search".labeloverrides the accessible name the button and the field share (defaults to the placeholder).inputProps: forwarded to the<input type="search">(value,onChange,name,ref…). Escape clears a controlled value through its ownonChange.children: trailing content inside the open field, such asInputSuffixwith aKbd.