Skip to main content
Version: v0.23.2

Filters bottom sheet

A "Filters" sheet with a sort order, a status select, a date range and Reset / Apply, all applied at once. The buttons take their colours and corners from usePickerTheme(), so they match the pickers in light and dark mode and in each design. It uses BottomSheet for the layout, HorizontalList for the sort order, and useDraft so nothing changes until Apply.

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

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

function Button({
  title,
  onPress,
  primary,
}: {
  title: string;
  onPress: () => void;
  primary?: boolean;
}) {
  const { colors, radius } = usePickerTheme();
  return (
    <Pressable
      onPress={onPress}
      style={[
        styles.btn,
        { borderRadius: radius.md, backgroundColor: primary ? colors.primary : colors.surface },
      ]}
    >
      <Text style={[styles.btnText, { color: primary ? colors.onPrimary : colors.text }]}>
        {title}
      </Text>
    </Pressable>
  );
}

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 (
    <>
      <Button
        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={<Button title="Reset" onPress={() => setDraft(EMPTY)} />}
        footer={
          <Button
            primary
            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 })}
            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' },
  btn: { paddingVertical: 12, paddingHorizontal: 16, alignItems: 'center' },
  btnText: { fontWeight: '600' },
});

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.