Skip to content

nsozturk/react-native-quick-filter

Repository files navigation

react-native-quick-filter

A reusable quick-filter flow for React Native & Expo — a horizontal chip / pill bar, chip dropdowns, and a slide-in filter panel with sort, faceted counts, a live result counter, i18n, and light / dark theming. Ships a headless core + a themed, ready-to-use UI. Distilled from the filter UX of Trendyol, Amazon, Flo and MediaMarkt.

npm version npm downloads MIT license TypeScript types included iOS Android Expo Runs with Expo Buy me a coffee


Screenshots

Chip bar + product list Dropdown (deferred apply) Master-detail panel (Amazon-style) Sheet, dark (MediaMarkt-style)
Quick filter chip bar Filter dropdown Master-detail filter panel Filter sheet dark

Why

Every e-commerce / catalog / search screen re-implements the same filtering UX: a scrollable chip bar, a way to pick values, an Apply step, a live result count, and active filters reflected back on the bar. react-native-quick-filter captures that as one domain-agnostic abstraction you can drop into any app.

  • Headless core + themed UI — zero-config out of the box, every part overridable.
  • Three detailed-filter presentations sharing one state: drilldown, master-detail (Amazon), and sheet (MediaMarkt).
  • Instant (toggle) and deferred (Apply/CTA) apply modes side by side.
  • Live result counter (resolveResultCount, sync or async) + faceted per-option counts.
  • Strict TypeScript, ships CJS + ESM + .d.ts.
  • i18n (English default, Turkish included) and light / dark theming.
  • Reanimated slide + swipe-to-dismiss (gesture-handler), reduce-motion aware.
  • Expo-friendly — JS-only, no native module; works in Expo Go and dev builds.

Installation

npm install react-native-quick-filter
# or: yarn add react-native-quick-filter  /  pnpm add react-native-quick-filter

Then install the peer dependencies (all in the Expo SDK):

npx expo install react-native-reanimated react-native-gesture-handler react-native-safe-area-context
peer dependency version
react >=18
react-native >=0.74
react-native-reanimated >=3.6 (v3 & v4)
react-native-gesture-handler >=2.14
react-native-safe-area-context >=4.8

App setup

Wrap your app root once, and make sure the Reanimated Babel plugin is enabled.

// App.tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';

export default function App() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <SafeAreaProvider>{/* your app */}</SafeAreaProvider>
    </GestureHandlerRootView>
  );
}
// babel.config.js (bare RN). The Reanimated plugin MUST be last.
module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: ['react-native-reanimated/plugin'], // v4: 'react-native-worklets/plugin'
};

With Expo, babel-preset-expo adds the Reanimated/worklets plugin automatically.

Quick start

import { useState } from 'react';
import {
  FilterProvider, QuickFilterBar, FilterDropdown, FilterSidePanel,
  type Facet, type ChipSpec, type Selection,
} from 'react-native-quick-filter';

const facets: Facet[] = [
  { id: 'sort', label: 'Sort', kind: 'single', options: [
      { id: 'recommended', label: 'Recommended' },
      { id: 'priceAsc', label: 'Price: Low to High' },
  ] },
  { id: 'category', label: 'Category', kind: 'multi', searchable: true, selectAll: true,
    options: [{ id: 'shoes', label: 'Shoes' }, { id: 'bags', label: 'Bags' }] },
  { id: 'brand', label: 'Brand', kind: 'multi', searchable: true, columns: 2,
    options: [{ id: 'nike', label: 'Nike' }, { id: 'adidas', label: 'Adidas' }] },
  { id: 'price', label: 'Price', kind: 'range', min: 0, max: 5000,
    presets: [{ id: 'p1', label: '$0 – $300', min: 0, max: 300 }] },
  { id: 'express', label: 'Express Delivery', kind: 'toggle',
    options: [{ id: 'on', label: 'Express Delivery' }] },
];

const chips: ChipSpec[] = [
  { facetId: 'filter', kind: 'action', label: 'Filter', opens: 'sidePanel' },
  { facetId: 'sort', kind: 'dropdown', label: 'Sort' },
  { facetId: 'category', kind: 'dropdown' },
  { facetId: 'brand', kind: 'dropdown' },
  { facetId: 'price', kind: 'dropdown' },
  { facetId: 'express', kind: 'toggle', removable: true },
];

function Screen() {
  const [selection, setSelection] = useState<Selection>();
  return (
    <FilterProvider
      facets={facets}
      chips={chips}
      onSelectionChange={setSelection}
      resolveResultCount={(sel) => fetchCount(sel)} // sync number or Promise<number>
    >
      <QuickFilterBar />
      {/* your product list, filtered by `selection` */}
      <FilterDropdown />
      <FilterSidePanel variant="masterDetail" side="left" />
    </FilterProvider>
  );
}

Detailed filter presentations

All three share the same state — whichever you render is what opens when the Filter chip is triggered.

// 1) drilldown — single panel, group list → tap → detail (with back). (Trendyol)
<FilterSidePanel variant="drilldown" side="right" />

// 2) masterDetail — left rail + right content, both visible at once. (Amazon)
<FilterSidePanel variant="masterDetail" side="left" animated={false} />

// 3) sheet — all facets as collapsible sections, pops over the page. (MediaMarkt)
//    presentation="fullScreen" for full screen; long sections scroll independently.
<FilterSheet presentation="sheet" sectionMaxHeight={280} />

Concepts

  • Facet — a filter dimension (single | multi | range | toggle), independent of presentation.
  • ChipSpec — a chip in the bar (action | dropdown | toggle | segment).
  • Selection — the single source of truth: { options: Record<id, string[]>, ranges: Record<id, { min?, max? }> }.
  • applyModeinstant (applies immediately) or deferred (draft → commit via Apply/CTA), per facet or provider-wide.

Theme & i18n

import { ThemeProvider, StringsProvider } from 'react-native-quick-filter';

<ThemeProvider scheme="dark" override={{ colors: { accent: '#F27A1A' } }}>
  <StringsProvider strings={{ apply: 'Apply', resultCount: (n) => `${n} items` }}>
    {/* ... */}
  </StringsProvider>
</ThemeProvider>

Without providers, the light theme + English dictionary defaults apply (zero-config). A Turkish dictionary ships built-in (import { tr } from 'react-native-quick-filter').

Controlled / uncontrolled

Pass a selection prop for controlled mode; omit it and use defaultSelection for uncontrolled. onSelectionChange(next) fires on every commit.

Headless usage

Build your own UI with only hooks — no UI components:

const { selection, activeCount, clearAll } = useFilter();
const brand = useFacet('brand'); // toggleOption, visibleOptions, commit, searchQuery, ...

API

Category Exports
Provider FilterProvider
Hooks useFilter, useFacet, useResultCount, useDropdown, usePanel, useChip
Bar / Chips QuickFilterBar, QuickFilterChip, Chip
Dropdown FilterDropdown
Detailed filter FilterSidePanel (variant: drilldown | masterDetail), FilterSheet (presentation: sheet | fullScreen), SidePanelMasterList, SidePanelRail
Bodies FacetBody, CheckboxListBody, RadioListBody, RangeBody
Atoms ResultCountButton, FilterFooter, Button, Checkbox, Radio, Badge, SearchInput, Chevron
Theme ThemeProvider, useTheme, defaultLightTheme, defaultDarkTheme, mergeTheme
i18n StringsProvider, useStrings, tr, en, formatNumber
Types Facet, FacetOption, ChipSpec, Selection, RangeValue, QuickFilterTheme, QuickFilterStrings, …

Full reference: docs/API.md · usage guide: docs/USAGE.md · consuming in another project: docs/CONSUMING.md.

For LLM tools (Claude Code, Cursor, GPT, etc.), a condensed machine-readable surface lives in llms.txt.

Compatibility

React Native >=0.74 (incl. New Architecture — JS-only, no native module)
Expo SDK 50+ · Expo Go & dev builds
Reanimated v3 (>=3.6) and v4
Platforms iOS · Android · (web via react-native-web, untested)
Types Bundled .d.ts (strict)

Development

npm run typecheck   # tsc --noEmit (strict)
npm test            # jest + @testing-library/react-native (36 tests)
npm run build       # react-native-builder-bob → lib/{module,commonjs,typescript}

Example app (Expo)

Three demos under example/: Store (light, master-detail panel), Electronics (dark, sheet, faceted counts), Headless (hooks only).

cd example
./scripts/sync-lib.sh   # packs & installs the library (re-run after source changes)
npx expo run:ios --device "iPhone 17 Pro Max"   # or: npx expo start

The example consumes the library as an npm pack tarball because it lives inside the package, which breaks Metro's symlink/out-of-tree resolution. A separate consumer app does not hit this — a normal install just works.

Keywords

React Native filter, Expo filter, quick filter, filter bar, filter chips, filter pills, faceted search, facets, sort menu, bottom-sheet filter, side panel / drawer filter, filter dropdown, e-commerce product filter, Trendyol/Amazon/MediaMarkt-style filter UI, headless filter, Reanimated filter.

Native ports

Same headless model, native UI on each platform:

☕ Support

If this library saves you time, you can support its development:

Buy me a coffee

Thank you! 🙏

Contributing

Issues and PRs welcome. Run npm run typecheck && npm test before opening a PR. See CHANGELOG.md for release history.

Design inspiration & analysis: references/00-GENEL-ANALIZ.md and per-app references/*/ANALYSIS.md.

License

MIT © enes ozturk

About

Reusable quick-filter flow for React Native & Expo: chip/pill bar, dropdowns, and a slide-in filter panel (drilldown, Amazon master-detail, MediaMarkt sheet) with sort, faceted counts, live result count, i18n and dark mode. Headless core + themed UI.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors