Search bar
Free · MITReact Native · ExpoAn iOS-style search field: focusing it shrinks the field while a Cancel button springs in beside it and scope chips cascade in below. Clear button, loading spinner and debounced queries built in.
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.Installation
$ npx shadcn@latest add https://goodui.dev/native/r/search-bar.jsonThen run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/search-bar.
Usage
import { useState } from "react";
import { SearchBar } from "@/components/goodui-native/search-bar/search-bar";
export function ContactSearch() {
const [query, setQuery] = useState("");
const [scope, setScope] = useState<"all" | "people" | "teams">("all");
return (
<SearchBar
placeholder="Search contacts"
debounce={250}
onChangeText={setQuery}
scopes={[
{ value: "all", label: "All" },
{ value: "people", label: "People" },
{ value: "teams", label: "Teams" },
]}
scope={scope}
onScopeChange={setScope}
/>
);
}When to use
Use it for
- Searching a list or the whole app: contacts, messages, products, transactions.
- Search that hits a server: set debounce and drive loading while results load.
- Search that can be narrowed to a category: pass scopes.
Reach for something else
- Form fields: use text-field.
- Switching views of the same data without typing: use segmented-control.
- A single always-visible filter row without a query: use chips or a segmented control.
Examples
Server search with a spinner
const [loading, setLoading] = useState(false);
<SearchBar
placeholder="Search transactions"
debounce={300}
loading={loading}
onChangeText={async (q) => {
setLoading(true);
setResults(await api.search(q));
setLoading(false);
}}
/>Navigation bar size without Cancel
<SearchBar size="sm" showCancel={false} placeholder="Filter" onChangeText={setFilter} />API reference
| Prop | Type | Default |
|---|---|---|
| value Query text (controlled). With `debounce`, the field keeps typing smoothly and syncs when this changes from outside. | string | — |
| defaultValue Initial query when uncontrolled. | string | "" |
| onChangeText Called with the query. Debounced when `debounce` is set; clear, cancel and submit always call it right away. | (text: string) => void | — |
| debounce Wait this many ms after the last keystroke before calling `onChangeText`. Default 0 (every keystroke). | number | 0 |
| onSubmit Called when the search key is pressed, with the current query. | (text: string) => void | — |
| onCancel Called after Cancel is pressed (the field has already blurred and, by default, cleared). | () => void | — |
| onActiveChange Called when the field becomes active (focused or holding a query) or inactive. | (active: boolean) => void | — |
| placeholder Placeholder. Same as `labels.placeholder`. | string | — |
| cancelLabel Label of the cancel button. Same as `labels.cancel`. | string | — |
| showCancel Reveal a Cancel button while the field is active. Default true. | boolean | true |
| clearOnCancel Cancel also clears the query. Default true. | boolean | true |
| scopes Scope chips under the field, for example All, People, Messages. | SearchScope<T>[] | — |
| scope Selected scope value. | T | — |
| onScopeChange Called with the tapped scope value. | (value: T) => void | — |
| scopesVisible `active` reveals the chips only while searching (like iOS); `always` keeps them visible. | "active" | "always" | "active" |
| loading Shows a spinner in place of the magnifier, for example while results load. | boolean | false |
| variant `filled` is a muted surface; `outline` adds a hairline border for plain backgrounds. | "filled" | "outline" | "filled" |
| size Heights 36 (`sm`, navigation bars) and 44 pt (`md`). | "sm" | "md" | "md" |
| autoFocus Focus the field on mount. | boolean | false |
| haptic `false` silences this bar's haptics (cancel, clear, scope change). | boolean | true |
| inputRef Ref to the underlying TextInput. | Ref<ComponentRef<typeof TextInput>> | — |
| accessibilityLabel Screen reader label. Defaults to the placeholder. | string | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<SearchBarLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- Focus: the field gives up exactly the room Cancel needs while Cancel slides in from the edge, both on the morph spring.
- The focus ring fades to the accent color.
- Scope chips: the row's height springs open and the chips cascade in 35 ms apart on the snappy spring; they fade out together when the bar deactivates.
- Clear scales in when there is text; chips scale down on press.
- Reduced motion: Cancel and the chip row appear with a short fade-speed timing instead of travelling, chips do not cascade or scale.
- Haptic: tap on Cancel and on clear
- Haptic: select when a different scope is chosen
- Layout changes (field width, chip row height) animate on the UI thread.
- With debounce, typing never re-renders the parent per keystroke; onChangeText fires after the pause.
Accessibility
- The TextInput has accessibilityRole search and is labelled by accessibilityLabel or the placeholder.
- Clear is a button labelled 'Clear search'; Cancel is a button with its visible label. Both are hidden from screen readers while they are not shown.
- Scope chips are tabs in a tablist with their selected state. The chip row is hidden from screen readers while collapsed.
- The search key submits (returnKeyType and enterKeyHint are search).
Notes for AI
- Set debounce (ms) instead of writing your own debounce around onChangeText. Clear, Cancel and submit always fire immediately.
- It works uncontrolled; pass value only to set the query from outside (a tapped recent search). With debounce, the field ignores its own echoed value.
- Cancel blurs and, by default, clears. Use clearOnCancel={false} to keep the query.
- scopesVisible='active' shows chips only while searching like iOS; use 'always' for a persistent filter.
- Inside a ScrollView set keyboardShouldPersistTaps='handled' so clear and chips work while the keyboard is up.
Related
Text field
A text field whose label glides up as you focus it, with helper and error text that slide in, a shake and haptic on error, clear and reveal buttons, a character counter and a multiline mode that grows with its text.
Segmented control
Two to five views of the same thing with an elastic highlight that stretches toward the new segment, can be dragged, and ticks at every boundary. Includes a billing toggle.