A native Apple TV search component built with SwiftUI's .searchable modifier. Drop it into your Expo tvOS app and get the system search experience — keyboard, Siri Remote, focus handling — out of the box.
- Native SwiftUI — uses
.searchablefor the real tvOS search experience, not a web imitation - Siri Remote support — full keyboard navigation, swipe, tap, and long-press handling on real hardware
- Configurable grid — portrait, landscape, square, or mini cards with adjustable columns, spacing, and padding
- Marquee titles — long titles auto-scroll on focus with configurable delay
- Image caching — async image loading with NSCache-backed caching
- Title overlay — gradient overlay with blur effect on card images, toggleable
- External titles — show title and subtitle below cards instead of (or alongside) the overlay
- Customizable colors — text color, accent/focus color, all via hex strings
- Color scheme override — force dark or light appearance regardless of system setting
- Controlled search text — set search field text programmatically for deep links, state restore, or "search for similar" flows
- Error & validation callbacks — structured error events and non-fatal validation warnings
- Focus callbacks —
onSearchFieldFocused/onSearchFieldBlurredfor gesture handler coordination - Platform-safe — renders
nullon non-tvOS platforms; useisNativeSearchAvailable()to gate rendering
npx expo install expo-tvos-searchOr install from GitHub:
npx expo install github:keiver/expo-tvos-searchYour project must be configured for React Native tvOS.
Platform requirements: tvOS 15.0+, Expo SDK 51+, React Native tvOS 0.71+
npm install react-native-tvos@latestnpx expo install @react-native-tvos/config-tvThen add the plugin in app.json / app.config.js:
{
"expo": {
"plugins": ["@react-native-tvos/config-tv"]
}
}import { TvosSearchView, type SearchResult } from 'expo-tvos-search';
const results: SearchResult[] = [
{
id: 'earth',
title: 'Earth',
subtitle: 'The Blue Marble',
imageUrl: 'https://example.com/earth.jpg',
},
{
id: 'mars',
title: 'Mars',
subtitle: 'The Red Planet',
imageUrl: 'https://example.com/mars.jpg',
},
];
export default function SearchScreen() {
return (
<TvosSearchView
results={results}
columns={4}
placeholder="Search planets..."
isLoading={false}
topInset={80}
onSearch={(e) => console.log('Search:', e.nativeEvent.query)}
onSelectItem={(e) => console.log('Selected:', e.nativeEvent.id)}
textColor="#E5E5E5"
accentColor="#E50914"
cardWidth={280}
cardHeight={420}
overlayTitleSize={18}
style={{ flex: 1 }}
/>
);
}On real Apple TV hardware, React Native's RCTTVRemoteHandler installs gesture recognizers that consume Siri Remote presses before they reach SwiftUI's .searchable text field, which prevents keyboard input. When the search field gains focus, this component temporarily disables touch cancellation using the official react-native-tvos notification API, and also disables tap/long-press recognizers from parent views (to cover cases like react-native-gesture-handler). Swipe and pan recognizers stay active for keyboard navigation. Everything is restored when focus leaves the field. This only applies to physical devices — the Simulator doesn't need it.
If this interferes with gesture handling in your app, please open an issue so we can sort it out.
For additional control, you can use the focus callbacks with TVEventControl:
import { TVEventControl } from 'react-native';
<TvosSearchView
onSearchFieldFocused={() => {
TVEventControl.disableGestureHandlersCancelTouches();
}}
onSearchFieldBlurred={() => {
TVEventControl.enableGestureHandlersCancelTouches();
}}
// ... other props
/>Apps with a fixed dark background can get illegible search bar text when the system is in light mode. Use colorScheme to force a specific appearance:
<TvosSearchView
colorScheme="dark"
// ... other props
/>"dark"— white text, dark UI elements (good for dark-background apps)"light"— black text, light UI elements"system"— follows the device setting (default, no override)
| Prop | Type | Default | Description |
|---|---|---|---|
results |
SearchResult[] |
[] |
Array of search results to display. Capped at 500 items. |
columns |
number |
5 |
Number of grid columns (clamped 1–10) |
placeholder |
string |
"Search..." |
Search field placeholder text |
searchText |
string |
— | Programmatically set search field text (for deep links, state restore) |
isLoading |
boolean |
false |
Shows a loading indicator |
| Prop | Type | Default | Description |
|---|---|---|---|
cardWidth |
number |
280 |
Width of each result card in points (clamped 50–1000) |
cardHeight |
number |
420 |
Height of each result card in points (clamped 50–1000) |
cardMargin |
number |
40 |
Spacing between cards (horizontal and vertical, clamped 0–200) |
cardPadding |
number |
16 |
Padding inside the card for overlay content (clamped 0–100) |
topInset |
number |
0 |
Top padding for tab bar clearance (clamped 0–500) |
| Prop | Type | Default | Description |
|---|---|---|---|
showTitle |
boolean |
false |
Show title below each result card |
showSubtitle |
boolean |
false |
Show subtitle below title |
showTitleOverlay |
boolean |
true |
Show title overlay with gradient at bottom of card |
showFocusBorder |
boolean |
false |
Show border on focused item |
imageContentMode |
'fill' | 'fit' | 'contain' |
'fill' |
How images fill the card: fill crops, fit/contain letterbox |
| Prop | Type | Default | Description |
|---|---|---|---|
textColor |
string |
system default | Color for text and UI elements (hex, e.g. "#FFFFFF") |
accentColor |
string |
"#FFC312" |
Accent color for focused elements (hex, e.g. "#E50914") |
colorScheme |
'light' | 'dark' | 'system' |
"system" |
Override the system color scheme for the search view |
overlayTitleSize |
number |
20 |
Font size for title text in the blur overlay (clamped 8–72) |
| Prop | Type | Default | Description |
|---|---|---|---|
enableMarquee |
boolean |
true |
Enable marquee scrolling for long titles |
marqueeDelay |
number |
1.5 |
Delay in seconds before marquee starts (clamped 0–60) |
| Prop | Type | Default | Description |
|---|---|---|---|
emptyStateText |
string |
"Search your library" |
Text shown when search field is empty |
searchingText |
string |
"Searching..." |
Text shown during search |
noResultsText |
string |
"No results found" |
Text shown when no results match |
noResultsHintText |
string |
"Try a different search term" |
Hint text below no results message |
| Prop | Type | Required | Description |
|---|---|---|---|
onSearch |
(event: SearchEvent) => void |
Yes | Called when search text changes |
onSelectItem |
(event: SelectItemEvent) => void |
Yes | Called when a result is selected |
onError |
(event: SearchViewErrorEvent) => void |
No | Called on errors (image loading, validation) |
onValidationWarning |
(event: ValidationWarningEvent) => void |
No | Called for non-fatal warnings (truncated fields, clamped values) |
onSearchFieldFocused |
(event: SearchFieldFocusEvent) => void |
No | Called when native search field gains focus |
onSearchFieldBlurred |
(event: SearchFieldFocusEvent) => void |
No | Called when native search field loses focus |
| Prop | Type | Default | Description |
|---|---|---|---|
style |
ViewStyle |
— | Style object for the view container |
interface SearchResult {
id: string; // Unique identifier (used in onSelectItem)
title: string; // Primary display text
subtitle?: string; // Optional secondary text
imageUrl?: string; // Optional poster/thumbnail URL (HTTPS, HTTP, file://, or data: URI)
}function isNativeSearchAvailable(): booleanReturns true when running on tvOS with the native module properly built. Use this to conditionally render a fallback on non-tvOS platforms.
import { TvosSearchView, isNativeSearchAvailable } from 'expo-tvos-search';
if (!isNativeSearchAvailable()) {
return <FallbackSearch />;
}
return <TvosSearchView {...props} />;The native implementation applies the following constraints:
- Maximum results — the array is capped at 500 items; extras are silently ignored
- Required fields — results with empty
idortitleare filtered out - Image URL schemes — HTTP, HTTPS,
file://, anddata:URIs are accepted; other schemes are rejected - HTTPS recommended — HTTP URLs may be blocked by App Transport Security unless explicitly allowed in Info.plist
Explore all configurations in the expo-tvos-search-demo repository.
npm test # Run tests once
npm run test:watch # Watch mode
npm run test:coverage # Generate coverage reportTests cover:
- Behavior across platforms
- Component rendering when native module is unavailable
- Event structure validation
We welcome contributions! Please see CONTRIBUTING.md for guidelines on:
- Code of conduct
- Development setup
- Testing requirements
- Commit message conventions
- Pull request process
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
MIT — see LICENSE for details.









