Components
SearchModal
Tapping search should open a real search surface. SearchModal is that surface: a full-screen sheet on phones, a centered command palette on desktop — with its own input, and a results area that's entirely yours.
Triggered from a SearchField
The intended flow: a SearchField in the header acts as the trigger via its onClick passthrough, and — optionally — hands over whatever was already typed through defaultQuery, which re-seeds the modal’s input on every open. Skip defaultQuery and the modal starts blank each time.
import { AppShell, Header, Content, SearchField, SearchModal } from "appshell-react";
import { useState } from "react";
export default function App() {
const [query, setQuery] = useState("");
const [open, setOpen] = useState(false);
return (
<AppShell safeArea>
<Header
behavior="reveal-search"
logo={<span className="font-bold">Docs</span>}
searchContent={
<SearchField
placeholder="Search docs"
value={query}
onChange={setQuery}
onClick={() => setOpen(true)} // tapping search opens the modal
/>
}
/>
<Content>{/* … */}</Content>
<SearchModal
open={open}
onClose={() => setOpen(false)}
defaultQuery={query} // optional: continue what was typed
placeholder="Search everything"
onSubmit={(value) => runSearch(value)}
>
{(q) => <Results query={q} />} {/* results are fully yours */}
</SearchModal>
</AppShell>
);
}Results are yours
The modal owns the input, the backdrop, Escape/backdrop dismissal, scroll locking, a Tab focus trap, and focus restore. Everything below the input row is children: pass a render function to build live results from the current query, or plain nodes for recents and suggestions. Nothing else is prescribed — list, grid, sections, empty states are your components.
// No Header, no AppShell — the modal is standalone.
<SearchModal open={open} onClose={close} placeholder="Search commands">
<RecentSearches />
</SearchModal>env(safe-area-inset-*) values, so the sheet clears notches and home indicators on real devices.API
| Prop | Type | Default | Description |
|---|---|---|---|
| open* | boolean | — | Whether the modal is visible. |
| onClose* | () => void | — | Called on Escape, backdrop click, or the cancel button. |
| query | string | — | Controlled query. Omit for uncontrolled. |
| defaultQuery | string | "" | Seeds the input every time the modal opens — pass the text typed into a triggering SearchField to continue the search seamlessly. |
| onQueryChange | (value: string) => void | — | Fires on every keystroke. |
| onSubmit | (value: string) => void | — | Fires with the current query when Enter is pressed. |
| placeholder | string | "Search" | Input placeholder and default accessible name. |
| children | ReactNode | (query: string) => ReactNode | — | Results area. A render function receives the live query; plain nodes work for static content (recents, suggestions). |
| closeLabel | string | "Cancel" | Label of the dismiss button. |
| className | string | — | Extra classes for the modal panel. |
| overlayClassName | string | — | Extra classes for the backdrop. |
| aria-label | string | — | Accessible dialog name override (defaults to the placeholder text). |