Skip to main content
Version: v0.30.x

Filters bottom sheet

A "Filters" sheet with a sort order, a status select, a date range and Reset / Apply, all applied at once. It uses BottomSheet for the layout, HorizontalList for the sort order, and useDraft so nothing changes until Apply.

Loading…
import { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import {
  BottomSheet,
  DatePicker,
  HorizontalList,
  SelectList,
  useDraft,
  formatDate,
  getRangePresets,
  usePickerTheme,
} from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
import { DemoButton } from './DemoButton';

type Sort = 'newest' | 'oldest' | 'priority';
type Filters = { sort: Sort; status: string[]; dates: DateRange };
const EMPTY: Filters = {
  sort: 'newest',
  status: [],
  dates: { start: null, end: null },
};
const SORTS: { label: string; value: Sort }[] = [
  { label: 'Newest', value: 'newest' },
  { label: 'Oldest', value: 'oldest' },
  { label: 'Priority', value: 'priority' },
];
const STATUSES = ['Open', 'In progress', 'Blocked', 'Done'].map((s) => ({
  label: s,
  value: s,
}));

export default function FiltersExample() {
  const { colors } = usePickerTheme();
  const [open, setOpen] = useState(false);
  const [filters, setFilters] = useState<Filters>(EMPTY);
  const [draft, setDraft] = useDraft(filters, open);
  const count = filters.status.length + (filters.dates.start ? 1 : 0);

  return (
    <>
      <DemoButton
        variant="secondary"
        title={`Filters${count ? ` · ${count}` : ''}`}
        onPress={() => setOpen(true)}
      />
      <Text style={[styles.summary, { color: colors.textSecondary }]}>
        {SORTS.find((s) => s.value === filters.sort)?.label} ·{' '}
        {filters.status.join(', ') || 'Any status'} ·{' '}
        {filters.dates.start && filters.dates.end
          ? `${formatDate(filters.dates.start)} → ${formatDate(filters.dates.end)}`
          : 'Any date'}
      </Text>

      <BottomSheet
        visible={open}
        onClose={() => setOpen(false)}
        title="Filters"
        height={0.85}
        headerRight={<DemoButton variant="text" title="Reset" onPress={() => setDraft(EMPTY)} />}
        footer={
          <DemoButton
            title="Apply"
            onPress={() => {
              setFilters(draft);
              setOpen(false);
            }}
          />
        }
      >
        <View style={styles.body}>
          <Text style={[styles.h, { color: colors.textSecondary }]}>Sort by</Text>
          <HorizontalList
            items={SORTS}
            value={draft.sort}
            onChange={(sort) => setDraft({ ...draft, sort })}
            accessibilityLabel="Sort by"
            style={styles.sort}
          />
          <Text style={[styles.h, { color: colors.textSecondary }]}>Status</Text>
          <SelectList
            multiple
            options={STATUSES}
            value={draft.status}
            onChange={(status) => setDraft({ ...draft, status })}
          />
          <Text style={[styles.h, { color: colors.textSecondary }]}>Created</Text>
          <DatePicker
            mode="range"
            value={draft.dates}
            onChange={(dates) => setDraft({ ...draft, dates })}
            presets={getRangePresets()}
            maxDate={new Date()}
            dayHeight={38}
          />
        </View>
      </BottomSheet>
    </>
  );
}

const styles = StyleSheet.create({
  body: { paddingHorizontal: 8 },
  h: {
    fontSize: 12,
    fontWeight: '700',
    textTransform: 'uppercase',
    marginTop: 12,
    marginBottom: 4,
    marginStart: 12,
  },
  sort: { marginHorizontal: 12 },
  summary: { marginTop: 8, textAlign: 'center' },
});

Why useDraft​

useDraft(filters, open) copies filters into draft each time the sheet opens. Edits change only the draft; Apply commits it, and closing any other way discards it. It's the same mechanism the built-in picker sheets use.

Scrolling content

With a fixed height, the sheet's content area is flex: 1. If your filters are taller than the sheet, wrap the body in a ScrollView.