KQ-1287 · SEARCH/EXPAND
Search Expand
An app header whose search icon grows into a field on the glide spring while the bar gives way to it frame by frame: the nav items nearest the field squeeze to their icons, then tuck into a More button that grows in with the first and squeezes on flick and snap as each lands, and the brand name steps aside before the last item goes — one motion value drives it all, and both rest states are rounded to whole items. The results panel then grows down out of the field itself on glide, its shell easing from a pill to a card with the matches rising in a cascade and a highlight that moves on snap, and Escape, a choice or blur unwinds it in reverse: the panel retracts on the exit ease before the field narrows and the items come back. The field is a combobox that `/` opens from anywhere, arrows move through results and Enter chooses; More is a menu button whose menu holds whatever is tucked.
Projects
4 projects
- Basin Road survey14 plots · Ines CalderActive
- Coldbrook wetland9 transects · Tomas ReyesReview
- Waylight orchard trial220 trees · Priya AnandDraft
- Gauge river flow6 stations · Bram OkaforActive
search closed · 6 of 6 in the bar
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/search-expand
Props
- nav
- SearchExpandItem[]
- default defaultSearchNav
- The bar's navigation in order (id, label, icon). The items nearest the search give way first.
- value / defaultValue
- string
- default the first item's id
- The current page. It carries the wash pill, which moves on snap; tucked, it hands over to a dot on More.
- onValueChange
- (id: string) => void
- default —
- Fires from the press or the More menu choice that asked for a page.
- index
- SearchExpandEntry[]
- default defaultSearchIndex
- What the search searches: id, title, group, meta, icon, keywords. Results are grouped in the order groups first appear here.
- recent
- string[]
- default defaultSearchRecent
- Searches offered while the field is empty; choosing one fills the field.
- onSelect
- (entry: SearchExpandEntry) => void
- default —
- A result was chosen by a press or Enter. The search closes after it.
- onQueryChange
- (query: string) => void
- default —
- Every edit of the field, with its text.
- open / defaultOpen
- boolean
- default false
- Whether the search is open. Controlled, every request goes through onOpenChange and the bar follows the host.
- onOpenChange
- (open: boolean) => void
- default —
- Fires from the press, key or blur that asked to open or close it.
- onTuckChange
- (ids: string[]) => void
- default —
- The ids tucked into More once the bar settles, whenever that set changes.
- width
- number
- default 320
- How wide the open field grows, in px. A wider field squeezes and tucks more of the nav; it never takes more than the bar can spare.
- results
- number
- default 5
- How many results the panel lists at most.
- tuck
- number
- default 0.5
- How much of the nav squeezes to its icons before anything tucks into More, 0 to 1: 0 tucks items whole, 1 squeezes every one first.
- brand
- React.ReactNode
- default a Fernworks mark
- The brand mark, about 24px, always shown.
- brandName
- string
- default "Fernworks"
- The product name beside the mark; it steps aside just before the last nav item tucks.
- actions
- React.ReactNode
- default —
- The account slot at the bar's far end: an avatar, a button.
- children
- React.ReactNode
- default —
- The page under the bar. The results panel and the More menu float over it and never reach past its bottom.
- placeholder
- string
- default "Search"
- The field's placeholder, and the search button's accessible name.
- label
- string
- default "Primary"
- The navigation's accessible name.
- moreLabel
- string
- default "More"
- The More button's name; its accessible name adds the count and whether the current page is inside.
- emptyLabel
- (query: string) => string
- default (q) => `No matches for “q”`
- The row shown when nothing matches.
- shortcut
- string | null
- default "/"
- The key that opens the search from anywhere on the page that is not typing; null for none.
- maxPanelHeight
- number
- default 320
- The panel's tallest, in px, when nothing sits under the bar to bound it; past it the panel scrolls inside itself.
- size
- "sm" | "md" | "lg"
- default "md"
- Bar height 40, 48 or 56 px; the field and the items scale with it.
- accent
- string
- default "var(--accent-bright)"
- The current-page pill, the result highlight, the matched letters and More's dot. Any CSS colour.
- sound
- boolean
- default false
- A swish as the visitor opens or closes the field, and clicks for choices, for each item landing in More and, softly, for arrow keys. Nothing plays unless the visitor did something.
- disabled
- boolean
- default false
- Dims the bar and disables the search, the nav and the shortcut.
- className
- string
- default —
- Merged onto the root (a container-query box holding the bar and the page).
| Prop | Type | Default | Description |
|---|---|---|---|
| nav | SearchExpandItem[] | defaultSearchNav | The bar's navigation in order (id, label, icon). The items nearest the search give way first. |
| value / defaultValue | string | the first item's id | The current page. It carries the wash pill, which moves on snap; tucked, it hands over to a dot on More. |
| onValueChange | (id: string) => void | — | Fires from the press or the More menu choice that asked for a page. |
| index | SearchExpandEntry[] | defaultSearchIndex | What the search searches: id, title, group, meta, icon, keywords. Results are grouped in the order groups first appear here. |
| recent | string[] | defaultSearchRecent | Searches offered while the field is empty; choosing one fills the field. |
| onSelect | (entry: SearchExpandEntry) => void | — | A result was chosen by a press or Enter. The search closes after it. |
| onQueryChange | (query: string) => void | — | Every edit of the field, with its text. |
| open / defaultOpen | boolean | false | Whether the search is open. Controlled, every request goes through onOpenChange and the bar follows the host. |
| onOpenChange | (open: boolean) => void | — | Fires from the press, key or blur that asked to open or close it. |
| onTuckChange | (ids: string[]) => void | — | The ids tucked into More once the bar settles, whenever that set changes. |
| width | number | 320 | How wide the open field grows, in px. A wider field squeezes and tucks more of the nav; it never takes more than the bar can spare. |
| results | number | 5 | How many results the panel lists at most. |
| tuck | number | 0.5 | How much of the nav squeezes to its icons before anything tucks into More, 0 to 1: 0 tucks items whole, 1 squeezes every one first. |
| brand | React.ReactNode | a Fernworks mark | The brand mark, about 24px, always shown. |
| brandName | string | "Fernworks" | The product name beside the mark; it steps aside just before the last nav item tucks. |
| actions | React.ReactNode | — | The account slot at the bar's far end: an avatar, a button. |
| children | React.ReactNode | — | The page under the bar. The results panel and the More menu float over it and never reach past its bottom. |
| placeholder | string | "Search" | The field's placeholder, and the search button's accessible name. |
| label | string | "Primary" | The navigation's accessible name. |
| moreLabel | string | "More" | The More button's name; its accessible name adds the count and whether the current page is inside. |
| emptyLabel | (query: string) => string | (q) => `No matches for “q”` | The row shown when nothing matches. |
| shortcut | string | null | "/" | The key that opens the search from anywhere on the page that is not typing; null for none. |
| maxPanelHeight | number | 320 | The panel's tallest, in px, when nothing sits under the bar to bound it; past it the panel scrolls inside itself. |
| size | "sm" | "md" | "lg" | "md" | Bar height 40, 48 or 56 px; the field and the items scale with it. |
| accent | string | "var(--accent-bright)" | The current-page pill, the result highlight, the matched letters and More's dot. Any CSS colour. |
| sound | boolean | false | A swish as the visitor opens or closes the field, and clicks for choices, for each item landing in More and, softly, for arrow keys. Nothing plays unless the visitor did something. |
| disabled | boolean | false | Dims the bar and disables the search, the nav and the shortcut. |
| className | string | — | Merged onto the root (a container-query box holding the bar and the page). |