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.

All files

128

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add expandable-search

Usage#

usage.tsxTSX
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.

sm
md
lg

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 for defaultOpen or 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, default w-64. The closed width is pinned by the size, so it never fights yours.
  • placeholder: default "Search". label overrides 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 own onChange.
  • children: trailing content inside the open field, such as InputSuffix with a Kbd.

FAQ#

When the row can't spare a field's width until someone actually searches: a table toolbar, a dense app header, a marketing navbar with a full link set. Where search is the main way around the product, keep the always-open field (an `Input` with an `InputPrefix`, or `NavbarSearch`) so nobody has to find it first.