Expanding search

An icon that morphs into a search field with results beneath it.

pnpm dlx shadcn@latest add @uiarc/expanding-search
Live · keyboard ready

NorthwindHandbook

Release checklist

What has to be true before we tag a release, and who signs off on each step.

  • Freeze main at noon on Thursday
  • Run the migration dry run on staging
  • Confirm the rollback plan with on call
  • Header or toolbar search where a full field would crowd the layout.
  • Quick navigation across local items with grouped results and recent suggestions.
  • Use search-field to filter a visible list in place.
  • Use combobox to set a form value.
  • Use command-palette for searching commands across the app.

Installation

Add Expanding search with the shadcn CLI, or copy the source by hand.

pnpm dlx shadcn@latest add @uiarc/expanding-search

Adds the component and its local dependencies, and installs motion, lucide-react. First time? Add the @uiarc registry to components.json, or use the full URL:

example.tsx
import { ExpandingSearch } from "@/components/arc/expanding-search/expanding-search"; export function HeaderSearch() {  const router = useRouter();  return (    <ExpandingSearch      label="Search projects and docs"      items={[        { id: "p1", title: "Arc website", group: "Projects", meta: "Updated today" },        { id: "d1", title: "Motion tokens", group: "Docs", keywords: ["spring"] },      ]}      onSelect={(item) => router.push(`/items/${item.id}`)}    />  );}

ExpandingSearch

An icon button that morphs into a search field with grouped, highlighted results beneath it.

PropTypeDefaultDescription
labelRequiredstring–Names the button, field, and results, such as "Search projects and docs".
itemsRequiredExpandingSearchItem[]–{ id, title, meta?, group?, icon?, keywords? }. Filtered client-side by title and keywords.
suggestionsExpandingSearchItem[][]Shown before anything is typed, such as recent searches.
suggestionsLabelstring"Recent"Heading for suggestions.
placeholderstring–Field placeholder. Defaults to label.
onSelect(item: ExpandingSearchItem) => void–Called when a result is chosen; the field then folds back.
onExpandedChange(expanded: boolean) => void–Called when the field opens or folds.
expandedWidthnumber360Widest the field grows, never past its container.
maxResultsnumber6Most results shown.
anchor"start" | "end""end"Edge the button sits on; the field grows away from it.
emptyHintstring"Try a shorter word or check the spelling."Tip under the empty state.
classNamestring–Added to the root.
EnterorSpace
On the icon button, expands the field.
ArrowDownorArrowUp
Moves through results, wrapping.
Enter
Chooses the active result.
Escape
Folds the field back into the icon.
  • The field is role="combobox" with aria-expanded, aria-controls, aria-autocomplete="list", and aria-activedescendant.
  • Results are a role="listbox" of role="option" items inside labelled role="group" sections.
  • Result counts and empty states are announced through a polite live region; the collapsed field is inert.
  • The button morphs into the field on a spring, results unfold beneath with a panel that tracks the field width, and a highlight travels between options.
  • Reduced motion swaps states with fades and no width or position travel.
  • The field grows to the smaller of expandedWidth and its container, and a ResizeObserver follows container resizes.
  • The results panel matches the field width and caps at min(22rem, 60vh), scrolling itself.
  • The idle lane takes no pointer events, so it never blocks what sits under it on small screens.
  • Ranking is memoized and runs over all items per query; pass pre-filtered or server results for large sets.
  • Result rows use layout position animation, capped by maxResults at 6 by default.

Notes for AI

Give your coding assistant the Markdown reference instead of screenshots.

  • Use in headers and toolbars where a full field would crowd the layout. Use search-field to filter a list in place and combobox to set a form value.
  • Place it in the space it may grow into; it fills that space up to expandedWidth. Filtering is local, so pass pre-fetched items or update items as the query changes.
  • Exported as both named and default.

The full library index for assistants is at /llms.txt.