Skip to content

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.

Open fullscreen
9:41
app.tsx
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.

standalone.tsx
// No Header, no AppShell — the modal is standalone.
<SearchModal open={open} onClose={close} placeholder="Search commands">
  <RecentSearches />
</SearchModal>
SearchModal renders through a portal on the client and renders nothing during SSR — safe to keep mounted in server-rendered pages. It positions itself with the platform’s env(safe-area-inset-*) values, so the sheet clears notches and home indicators on real devices.

API

PropTypeDefaultDescription
open*boolean—Whether the modal is visible.
onClose*() => void—Called on Escape, backdrop click, or the cancel button.
querystring—Controlled query. Omit for uncontrolled.
defaultQuerystring""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.
placeholderstring"Search"Input placeholder and default accessible name.
childrenReactNode | (query: string) => ReactNode—Results area. A render function receives the live query; plain nodes work for static content (recents, suggestions).
closeLabelstring"Cancel"Label of the dismiss button.
classNamestring—Extra classes for the modal panel.
overlayClassNamestring—Extra classes for the backdrop.
aria-labelstring—Accessible dialog name override (defaults to the placeholder text).