PopoverFilter

free

PopoverFilter combines a search input and checkbox list behind a popover trigger, for filtering a list or table by more than one value.

@floating-ui/react
Preview
Dark
import { useState } from 'react';
import PopoverFilter from '@/components/composites/PopoverFilter';

const priorityData = [
  { label: 'Urgent', value: 'urgent' },
  { label: 'High', value: 'high' },
  { label: 'Medium', value: 'medium' },
  { label: 'Low', value: 'low' },
  { label: 'Backlog', value: 'backlog' },
];

export default function UsageDemo() {
  const [selected, setSelected] = useState<string[]>([]);

  const title =
    selected.length === 0
      ? 'Priority'
      : selected.length === 1
        ? priorityData.find((item) => item.value === selected[0])?.label
        : `${selected.length} selected`;

  return (
    <div className="flex justify-center">
      <PopoverFilter
        data={priorityData}
        title={title}
        value={selected}
        onChange={(data) => setSelected(data.map((item) => item.value))}
      />
    </div>
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add PopoverFilter

Examples

Custom Trigger

Preview
Dark
import { useState } from 'react';
import { PiFunnel } from 'react-icons/pi'
import Button from '@/components/ui/Button';
import PopoverFilter from '@/components/composites/PopoverFilter';

const teamData = [
  { label: 'Design', value: 'design' },
  { label: 'Engineering', value: 'engineering' },
  { label: 'Product', value: 'product' },
  { label: 'Marketing', value: 'marketing' },
  { label: 'Sales', value: 'sales' },
  { label: 'Support', value: 'support' },
];

export default function CustomTriggerDemo() {
  const [selected, setSelected] = useState<string[]>(['engineering']);

  return (
    <div className="flex justify-center">
      <PopoverFilter
        data={teamData}
        value={selected}
        onChange={(data) => setSelected(data.map((item) => item.value))}
        renderTrigger={
          <Button icon={<PiFunnel />}>
            Team{selected.length > 0 && ` (${selected.length})`}
          </Button>
        }
      />
    </div>
  );
}

Hide Reset

Preview
Dark
import PopoverFilter from '@/components/composites/PopoverFilter';

const regionData = [
  { label: 'North America', value: 'na' },
  { label: 'Europe', value: 'eu' },
  { label: 'Asia Pacific', value: 'apac' },
  { label: 'Latin America', value: 'latam' },
];

export default function ShowResetDemo() {
  return (
    <div className="flex justify-center">
      <PopoverFilter
        data={regionData}
        title="Region"
        showReset={false}
      />
    </div>
  );
}

Placement

Preview
Dark
import PopoverFilter from '@/components/composites/PopoverFilter';

const typeData = [
  { label: 'Bug', value: 'bug' },
  { label: 'Feature', value: 'feature' },
  { label: 'Task', value: 'task' },
  { label: 'Chore', value: 'chore' },
];

export default function PlacementDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-8">
      <PopoverFilter
        data={typeData}
        title="Bottom Start"
        placement="bottom-start"
      />
      <PopoverFilter 
        data={typeData} 
        title="Bottom End" 
        placement="bottom-end" 
      />
    </div>
  );
}

API

PopoverFilter wraps Popover with a searchable checkbox list. onChange receives the matching subset of data, not the raw selected values — pair it with .map((item) => item.value) to recover a plain string array (see Usage). When renderTrigger is set it fully replaces the default trigger button, so title has no visible effect. A search term with no matches renders a plain "No results found" message in place of the checkbox list.

PropDescriptionTypeDefault
dataFilter options rendered as checkboxes.PopoverFilterOption[]
valueControlled selected values. Re-syncs the internal selection whenever it changes.string[]
onChangeCalled with the subset of data matching the new selection — not the raw selected values.(data: PopoverFilterOption[]) => void
titleLabel for the default trigger button. Ignored when renderTrigger is set.string | ReactNode'Filter'
renderTriggerCustom trigger element, replacing the default button entirely.ReactNode
placementSide the popover opens from, relative to the trigger.'bottom-start' | 'bottom-end''bottom-start'
inputPlaceholderPlaceholder text for the search input.string'Search...'
showResetShow a "Clear filters" button once at least one option is selected.booleantrue
widthWidth of the popover panel, in pixels.number220

Types

type PopoverFilterOption = {
  label: string;
  value: string;
}