npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-tvos-search

v1.5.2

Published

Native tvOS search view using SwiftUI .searchable modifier for Expo/React Native

Readme

expo-tvos-search

npm version License: MIT Test Status

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.

Features

  • Native SwiftUI — uses .searchable for 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 callbacksonSearchFieldFocused / onSearchFieldBlurred for gesture handler coordination
  • Platform-safe — renders null on non-tvOS platforms; use isNativeSearchAvailable() to gate rendering

Table of Contents

Installation

npx expo install expo-tvos-search

Or install from GitHub:

npx expo install github:keiver/expo-tvos-search

Prerequisites for tvOS Builds

Your project must be configured for React Native tvOS.

Platform requirements: tvOS 15.0+, Expo SDK 51+, React Native tvOS 0.71+

1. Install react-native-tvos

npm install react-native-tvos@latest

2. Install the tvOS config plugin

npx expo install @react-native-tvos/config-tv

Then add the plugin in app.json / app.config.js:

{
  "expo": {
    "plugins": ["@react-native-tvos/config-tv"]
  }
}

Quick Start

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 }}
    />
  );
}

Apple TV Hardware Keyboard Support

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
/>

Color Scheme Override

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)

API Reference

Props

Core

| 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 |

Card Dimensions & Spacing

| 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) |

Display Options

| 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 |

Styling & Colors

| 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) |

Animation

| 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) |

Text Customization

| 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 |

Event Handlers

| 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 |

Other

| Prop | Type | Default | Description | |------|------|---------|-------------| | style | ViewStyle | — | Style object for the view container |

SearchResult

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, or data: URI)
}

isNativeSearchAvailable()

function isNativeSearchAvailable(): boolean

Returns 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} />;

Result Validation

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 id or title are filtered out
  • Image URL schemes — HTTP, HTTPS, and data: URIs are accepted; other schemes (e.g. file://) are rejected
  • HTTPS recommended — HTTP URLs may be blocked by App Transport Security unless explicitly allowed in Info.plist

Demo App

Explore all configurations in the expo-tvos-search-demo repository.

Testing

npm test                # Run tests once
npm run test:watch      # Watch mode
npm run test:coverage   # Generate coverage report

Tests cover:

  • Behavior across platforms
  • Component rendering when native module is unavailable
  • Event structure validation

Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines on:

  • Code of conduct
  • Development setup
  • Testing requirements
  • Commit message conventions
  • Pull request process

License

MIT — see LICENSE for details.