Components
SearchField
The search input, solved once: a rounded pill or an edge-to-edge bar for the Header's search row, theme-aware and controlled or uncontrolled.
Pill (default)
The classic mobile search bar — a rounded, inset field with its own horizontal padding, ready to drop into searchContent.
import { AppShell, Header, Content, SearchField } from "appshell-react";
import { useState } from "react";
export default function App() {
const [query, setQuery] = useState("");
return (
<AppShell safeArea>
<Header
behavior="reveal-search"
logo={<span className="font-bold">Market</span>}
searchContent={
<SearchField
placeholder="Search 2,400 products"
value={query}
onChange={setQuery}
onSubmit={(value) => console.log("search:", value)}
/>
}
/>
<Content>{/* filtered results */}</Content>
</AppShell>
);
}Full-width
variant="full" spans the entire row as a flat bar with hairline borders — no side margins, no rounding. Use it when search is the primary action of the screen.
<Header
behavior="fixed"
logo={<span className="font-bold">Library</span>}
searchContent={
// Flat bar spanning the whole row, edge to edge
<SearchField variant="full" placeholder="Search the catalog" />
}
/>Where does search go on desktop?
The full-row search bar is a mobile pattern. On desktop, follow the layout: no sidebar — keep a compact pill in the Header (its centered max width handles wide screens) and give it a shortcutHint like "⌘K", bound with useSearchShortcut; docked sidebar — move the pill into the Sidebar’s topContent slot with inset={false}, where it fills the panel’s otherwise-empty top and leaves the header clean. The Docked Sidebar demo shows the whole arrangement live.
Theme awareness
Inside a theme="primary" or theme="dark" Header, the field switches to translucent light-on-dark surfaces automatically via useHeaderTheme() — no extra classes needed.
light, so SearchField works anywhere a search box is needed — a sidebar, a page body, a dialog.API
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "pill" | "full" | "pill" | "pill" is the rounded inset field; "full" spans the row edge to edge with no surface of its own, taking the background it sits on so the search row reads as part of the header. |
| placeholder | string | "Search" | Placeholder and default accessible name. |
| value | string | — | Controlled value. |
| defaultValue | string | — | Uncontrolled initial value. |
| onChange | (value: string) => void | — | Fires on every keystroke with the new value. |
| onSubmit | (value: string) => void | — | Fires with the current value when Enter is pressed. |
| onClick | () => void | — | Click/tap passthrough — the natural place to open a SearchModal (a click, unlike focus, can't retrigger when the modal restores focus on close). |
| onFocus | () => void | — | Focus passthrough on the input. |
| shortcutHint | ReactNode | — | Keyboard-shortcut chip at the right edge — "⌘K", "Ctrl K". Hidden below the sm breakpoint; pair with useSearchShortcut. |
| inset | boolean | true | Pill only. Header-row insetting (padding + centered max width). Set false when the pill lives somewhere already padded — a Sidebar's topContent slot, a card. |
| className | string | — | Extra classes for the field surface. |
| inputClassName | string | — | Extra classes for the inner input element. |
| aria-label | string | — | Accessible name override (defaults to the placeholder). |