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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@atom_design/search

v1.0.1

Published

A comprehensive React Native search component with filters, debouncing, and customizable results.

Readme

@atom_design/search

A comprehensive and flexible Search Component for React Native, built as part of the Atom Design System. Features a search bar with debouncing, filter chips, and customizable results list.


✨ Features

  • 🔍 Search Bar - Clean input with search icon and clear button
  • ⏱️ Debounced Search - Configurable delay to reduce API calls
  • 🏷️ Filter Chips - Multi-select filter tags with checkmarks
  • 📋 Results List - FlatList with custom rendering support
  • ⌨️ Keyboard Handling - Auto-dismiss and return key support
  • 🔄 Loading State - Spinner indicator during search
  • 🎨 Fully Customizable - Colors, styles, and components
  • Accessible - Screen reader support with proper roles
  • 🎯 TypeScript Support - Full type definitions included

📦 Installation

npm install @atom_design/search

Or with yarn:

yarn add @atom_design/search

Peer Dependencies

Make sure you have the required peer dependencies installed:

npm install react-native-vector-icons prop-types

For react-native-vector-icons setup, follow the installation guide.


📥 Import

import Search from '@atom_design/search';
// or
import { Search } from '@atom_design/search';

🎯 Basic Usage

import React, { useState, useMemo } from 'react';
import Search from '@atom_design/search';

function SearchExample() {
  const [results, setResults] = useState([]);

  const handleSearch = (query) => {
    // Filter or fetch results based on query
    const filtered = allItems.filter(item =>
      item.label.toLowerCase().includes(query.toLowerCase())
    );
    setResults(filtered);
  };

  return (
    <Search
      placeholder="Search items..."
      onSearch={handleSearch}
      results={results}
      onResultPress={(item) => console.log('Selected:', item)}
    />
  );
}

📋 Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | placeholder | string | 'Search...' | Placeholder text | | value | string | - | Controlled input value | | onSearch | function | - | Debounced search callback | | onChangeText | function | - | Immediate text change callback | | results | array | [] | Array of result items | | filters | array | [] | Array of filter options | | selectedFilters | array | - | Controlled selected filters | | onFilterSelect | function | - | Filter change callback | | onResultPress | function | - | Result item press callback | | renderResult | function | - | Custom result renderer | | loading | boolean | false | Show loading indicator | | debounceDelay | number | 300 | Debounce delay in ms | | showClearButton | boolean | true | Show clear button | | showSearchIcon | boolean | true | Show search icon | | emptyText | string | 'No results found' | Empty state text | | emptyComponent | element | - | Custom empty component | | primaryColor | string | '#d9232d' | Primary/accent color | | disabled | boolean | false | Disable the component | | autoFocus | boolean | false | Auto focus on mount | | keyboardType | string | 'default' | Keyboard type | | returnKeyType | string | 'search' | Return key type | | onSubmitEditing | function | - | Submit callback | | containerStyle | ViewStyle | - | Container style | | searchBarStyle | ViewStyle | - | Search bar style | | inputStyle | TextStyle | - | Input text style | | filtersContainerStyle | ViewStyle | - | Filters container style | | resultsContainerStyle | ViewStyle | - | Results list style | | testID | string | - | Test ID |


🎨 Customization Examples

With Filters

<Search
  placeholder="Search products..."
  onSearch={handleSearch}
  results={results}
  filters={['All', 'Electronics', 'Clothing', 'Home']}
  onFilterSelect={(filters) => {
    console.log('Active filters:', filters);
    // Re-fetch with filters
  }}
/>

With Loading State

const [loading, setLoading] = useState(false);

const handleSearch = async (query) => {
  setLoading(true);
  const data = await fetchFromAPI(query);
  setResults(data);
  setLoading(false);
};

<Search
  onSearch={handleSearch}
  results={results}
  loading={loading}
/>

Custom Result Rendering

<Search
  onSearch={handleSearch}
  results={results}
  renderResult={({ item }) => (
    <TouchableOpacity style={styles.customResult}>
      <Image source={{ uri: item.image }} style={styles.image} />
      <View>
        <Text style={styles.title}>{item.title}</Text>
        <Text style={styles.subtitle}>{item.category}</Text>
      </View>
      <Text style={styles.price}>${item.price}</Text>
    </TouchableOpacity>
  )}
/>

Controlled Mode

const [query, setQuery] = useState('');
const [filters, setFilters] = useState([]);

<Search
  value={query}
  onChangeText={setQuery}
  selectedFilters={filters}
  onFilterSelect={setFilters}
  results={results}
/>

Custom Colors

<Search
  onSearch={handleSearch}
  results={results}
  filters={['All', 'Active', 'Completed']}
  primaryColor="#4CAF50"
/>

No Debounce (Instant Search)

<Search
  onSearch={handleSearch}
  results={results}
  debounceDelay={0}
/>

🧪 Test Screen Example

import React, { useState, useMemo } from 'react';
import { View, Text, ScrollView, StyleSheet, SafeAreaView } from 'react-native';
import Search from '@atom_design/search';

const SearchTestScreen = () => {
  const [query, setQuery] = useState('');
  const [selectedFilters, setSelectedFilters] = useState([]);
  const [loading, setLoading] = useState(false);
  const [basicQuery, setBasicQuery] = useState('');

  // Sample data
  const allProducts = [
    { id: '1', label: 'Industrial Safety Helmet', category: 'Safety' },
    { id: '2', label: 'Work Gloves - Heavy Duty', category: 'Safety' },
    { id: '3', label: 'Power Drill 20V', category: 'Tools' },
    { id: '4', label: 'LED Work Light', category: 'Tools' },
    { id: '5', label: 'Steel Toe Boots', category: 'Footwear' },
    { id: '6', label: 'Welding Mask Auto-Darkening', category: 'Safety' },
    { id: '7', label: 'Measuring Tape 25ft', category: 'Tools' },
    { id: '8', label: 'Safety Goggles', category: 'Safety' },
    { id: '9', label: 'Cordless Angle Grinder', category: 'Tools' },
    { id: '10', label: 'Anti-Slip Work Shoes', category: 'Footwear' },
  ];

  const filters = ['All', 'Safety', 'Tools', 'Footwear'];

  // Filter results for basic search
  const basicResults = useMemo(() => {
    if (!basicQuery) return allProducts;
    return allProducts.filter(item =>
      item.label.toLowerCase().includes(basicQuery.toLowerCase())
    );
  }, [basicQuery]);

  // Filter results based on query and filters
  const filteredResults = useMemo(() => {
    let results = allProducts;

    // Apply text search
    if (query) {
      results = results.filter(item =>
        item.label.toLowerCase().includes(query.toLowerCase())
      );
    }

    // Apply category filters
    if (selectedFilters.length > 0 && !selectedFilters.includes('All')) {
      results = results.filter(item =>
        selectedFilters.includes(item.category)
      );
    }

    return results;
  }, [query, selectedFilters]);

  // Handle search callback
  const handleSearch = (searchQuery, activeFilters) => {
    console.log('Search:', searchQuery, 'Filters:', activeFilters);
  };

  const handleResultPress = (item) => {
    console.log('Selected product:', item);
    // Navigate to product details
  };

  return (
    <SafeAreaView style={styles.container}>
      <ScrollView>
        <Text style={styles.header}>Atom Design - Search</Text>

        {/* Basic Search */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>Basic Search</Text>
          <Search
            placeholder="Search products..."
            value={basicQuery}
            onChangeText={setBasicQuery}
            onSearch={handleSearch}
            results={basicResults}
            onResultPress={handleResultPress}
          />
          <Text style={styles.info}>
            Showing {basicResults.length} of {allProducts.length} products
          </Text>
        </View>

        {/* With Filters */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>Search with Filters</Text>
          <Search
            placeholder="Search by name..."
            value={query}
            onChangeText={setQuery}
            onSearch={handleSearch}
            results={filteredResults}
            filters={filters}
            selectedFilters={selectedFilters}
            onFilterSelect={(newFilters) => setSelectedFilters(newFilters)}
            onResultPress={handleResultPress}
          />
          <Text style={styles.info}>
            Found {filteredResults.length} results
            {selectedFilters.length > 0 && ` in ${selectedFilters.join(', ')}`}
          </Text>
        </View>

        {/* Loading State */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>Loading State Demo</Text>
          <Search
            placeholder="Type to search..."
            loading={loading}
            onSearch={() => {
              setLoading(true);
              setTimeout(() => setLoading(false), 1500);
            }}
            results={[]}
            debounceDelay={500}
          />
        </View>

        {/* Custom Primary Color */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>Custom Color</Text>
          <Search
            placeholder="Search..."
            results={filteredResults.slice(0, 3)}
            filters={['Option A', 'Option B', 'Option C']}
            primaryColor="#2196F3"
            onResultPress={handleResultPress}
          />
        </View>

        {/* Disabled State */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>Disabled State</Text>
          <Search
            placeholder="Search disabled..."
            disabled
            results={[]}
          />
        </View>

        {/* Custom Empty State */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>Custom Empty Message</Text>
          <Search
            placeholder="Search something..."
            value="xyz123"
            results={[]}
            emptyText="No products match your search. Try different keywords."
          />
        </View>
      </ScrollView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  header: {
    fontSize: 24,
    fontWeight: 'bold',
    textAlign: 'center',
    marginVertical: 20,
    color: '#1A1A1A',
  },
  section: {
    backgroundColor: '#FFFFFF',
    marginHorizontal: 16,
    marginBottom: 16,
    padding: 16,
    borderRadius: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  sectionTitle: {
    fontSize: 18,
    fontWeight: '600',
    marginBottom: 12,
    color: '#1A1A1A',
  },
  info: {
    marginTop: 8,
    fontSize: 13,
    color: '#666',
    fontStyle: 'italic',
  },
});

export default SearchTestScreen;

📱 Result Item Format

Results should be objects with at least one of these properties:

const results = [
  { id: '1', label: 'Product Name' },      // Uses 'label'
  { id: '2', title: 'Product Title' },     // Uses 'title'
  { id: '3', name: 'Product' },            // Uses 'name'
];

For custom rendering, use the renderResult prop.


♿ Accessibility

The Search component includes comprehensive accessibility support:

  • Search input uses accessibilityRole="search"
  • Filter chips use accessibilityRole="tab" with selection state
  • Result items use accessibilityRole="button"
  • Clear button has accessibilityLabel="Clear search"
  • Empty state is announced to screen readers

📝 TypeScript

Full TypeScript definitions are included:

import Search, { SearchProps, SearchResultItem } from '@atom_design/search';

interface Product extends SearchResultItem {
  price: number;
  category: string;
}

const MySearch: React.FC = () => {
  const [results, setResults] = useState<Product[]>([]);

  const handleSearch = (query: string, filters: string[]): void => {
    // Fetch results
  };

  return (
    <Search
      onSearch={handleSearch}
      results={results}
      filters={['All', 'Category A']}
    />
  );
};

📄 License

MIT © Atom Design


👤 Author

Atom Design Team

Part of the Atom Design System for React Native.