Mantine Composite Filters

mantine-composite-filters

A powerful, flexible composite filters component for Mantine with support for multiple filter types, presets, history, and advanced customization

Installation

yarn add mantine-composite-filters

Overview

Build powerful filter interfaces in minutes. CompositeFiltersInput brings LinkedIn-style composite filtering to your Mantine apps - complete with type-safe definitions, smart operators, presets, and full keyboard support.

Why use this?

  • Type any field name to start filtering
  • Smart operator selection based on data type
  • Save filter combinations as reusable presets
  • Full keyboard navigation for power users
  • Zero configuration localStorage persistence

Quick Start

import { useState } from 'react';
import { CompositeFiltersInput } from 'mantine-composite-filters';
import type { ActiveFilter, FilterDefinition } from 'mantine-composite-filters';

const filters: FilterDefinition[] = [
  {
    key: 'name',
    label: 'Name',
    type: 'text',
    placeholder: 'Search...',
    operators: ['contains', 'starts_with', '='],
  },
  {
    key: 'email',
    label: 'Email',
    type: 'email',
  },
  {
    key: 'status',
    label: 'Status',
    type: 'select',
    options: [
      { value: 'active', label: 'Active' },
      { value: 'pending', label: 'Pending' },
      { value: 'inactive', label: 'Inactive' },
    ],
  },
  {
    key: 'tags',
    label: 'Tags',
    type: 'multi_select',
    options: [
      { value: 'vip', label: 'VIP' },
      { value: 'new', label: 'New' },
      { value: 'verified', label: 'Verified' },
    ],
  },
];

function Demo() {
  const [activeFilters, setActiveFilters] = useState<ActiveFilter[]>([]);

  return (
    <CompositeFiltersInput
      filters={filters}
      value={activeFilters}
      onChange={setActiveFilters}
      placeholder="Filter by..."
    />
  );
}

Three props to get started:

<CompositeFiltersInput
  filters={filterDefinitions}  // What fields can be filtered
  value={activeFilters}        // Current filter state
  onChange={setActiveFilters}  // State updater
/>

Interactive Playground

Options

import { useState } from 'react';
import { CompositeFiltersInput } from 'mantine-composite-filters';
import type { ActiveFilter, FilterDefinition } from 'mantine-composite-filters';

const filters: FilterDefinition[] = [
  { key: 'name', label: 'Name', type: 'text' },
  { key: 'email', label: 'Email', type: 'email' },
  { key: 'status', label: 'Status', type: 'select', options: [
    { value: 'active', label: 'Active' },
    { value: 'pending', label: 'Pending' },
  ]},
];

function Demo() {
  const [activeFilters, setActiveFilters] = useState<ActiveFilter[]>([]);

  return (
    <CompositeFiltersInput
      filters={filters}
      value={activeFilters}
      onChange={setActiveFilters}
      placeholder="Filter..."
      maxFilters={undefined}
      overflowMode="scroll"
      disablePresets={false}
      disableHistory={false}
    />
  );
}

Filter Types

Seven built-in types cover most filtering needs:

text
email
number
select
multi_select
date
date_range
import { useState } from 'react';
import { CompositeFiltersInput } from 'mantine-composite-filters';
import type { ActiveFilter, FilterDefinition } from 'mantine-composite-filters';

// All 7 filter types
const allFilters: FilterDefinition[] = [
  {
    key: 'text_field',
    label: 'Text',
    type: 'text',
    placeholder: 'Enter text...',
    operators: ['contains', 'starts_with', 'ends_with', '='],
  },
  {
    key: 'email_field',
    label: 'Email',
    type: 'email',
    placeholder: 'user@email.com',
  },
  {
    key: 'number_field',
    label: 'Number',
    type: 'number',
    placeholder: 'Enter number...',
    operators: ['=', '!=', '>', '<', '>=', '<='],
  },
  {
    key: 'select_field',
    label: 'Select',
    type: 'select',
    options: [
      { value: 'option1', label: 'Option 1' },
      { value: 'option2', label: 'Option 2' },
    ],
  },
  {
    key: 'multi_select_field',
    label: 'Multi Select',
    type: 'multi_select',
    options: [
      { value: 'tag1', label: 'Tag 1' },
      { value: 'tag2', label: 'Tag 2' },
    ],
  },
  {
    key: 'date_field',
    label: 'Date',
    type: 'date',
  },
  {
    key: 'date_range_field',
    label: 'Date Range',
    type: 'date_range',
  },
];

function Demo() {
  const [filters, setFilters] = useState<ActiveFilter[]>([]);

  return (
    <CompositeFiltersInput
      filters={allFilters}
      value={filters}
      onChange={setFilters}
      placeholder="Try each filter type..."
    />
  );
}
TypeUse CaseDefault Operators
textNames, titles, descriptionscontains starts_with ends_with =
emailEmail addresses (with validation)contains =
numberPrices, quantities, scores= != > < >= <=
selectStatus, category, single choice=
multi_selectTags, roles, multiple choices=
dateSingle date values= > < >= <=
date_rangeDate periods, rangesbetween

Limiting Filters

Control complexity with maxFilters:

import { useState } from 'react';
import { CompositeFiltersInput } from 'mantine-composite-filters';
import type { ActiveFilter, FilterDefinition } from 'mantine-composite-filters';

const filters: FilterDefinition[] = [
  { key: 'name', label: 'Name', type: 'text' },
  { key: 'email', label: 'Email', type: 'email' },
  { key: 'age', label: 'Age', type: 'number' },
  { key: 'status', label: 'Status', type: 'select', options: [
    { value: 'active', label: 'Active' },
    { value: 'inactive', label: 'Inactive' },
  ]},
];

function Demo() {
  const [activeFilters, setActiveFilters] = useState<ActiveFilter[]>([]);

  return (
    <CompositeFiltersInput
      filters={filters}
      value={activeFilters}
      onChange={setActiveFilters}
      maxFilters={3}
      placeholder="Filter by... (max 3 filters)"
    />
  );
}

Layout Options

Two overflow modes for different UI needs:

Scroll Mode (default)

Filters scroll horizontally when they exceed the container width

Wrap Mode

Filters wrap to multiple lines when they exceed the container width

import { useState } from 'react';
import { CompositeFiltersInput } from 'mantine-composite-filters';
import type { ActiveFilter, FilterDefinition } from 'mantine-composite-filters';

const filters: FilterDefinition[] = [
  { key: 'name', label: 'Name', type: 'text' },
  { key: 'email', label: 'Email', type: 'email' },
  { key: 'status', label: 'Status', type: 'select', options: [
    { value: 'active', label: 'Active' },
    { value: 'inactive', label: 'Inactive' },
  ]},
];

function Demo() {
  const [filters1, setFilters1] = useState<ActiveFilter[]>([]);
  const [filters2, setFilters2] = useState<ActiveFilter[]>([]);

  return (
    <>
      {/* Scroll Mode - filters scroll horizontally */}
      <CompositeFiltersInput
        filters={filters}
        value={filters1}
        onChange={setFilters1}
        overflowMode="scroll"
        placeholder="Scroll mode"
      />

      {/* Wrap Mode - filters wrap to new lines */}
      <CompositeFiltersInput
        filters={filters}
        value={filters2}
        onChange={setFilters2}
        overflowMode="wrap"
        placeholder="Wrap mode"
      />
    </>
  );
}
ModeBehaviorBest For
scrollHorizontal scroll, input stays visibleNarrow containers, toolbars
wrapFilters wrap to new linesWide containers, dashboards

Presets & History

Let users save and recall filter combinations:

Click the ⋮ menu to:

  • Save current filters as a preset
  • Load saved presets
  • Mark presets as favorites
  • View filter history

Data persists in browser localStorage

import { useState } from 'react';
import { CompositeFiltersInput } from 'mantine-composite-filters';
import type { ActiveFilter, FilterDefinition } from 'mantine-composite-filters';

const filters: FilterDefinition[] = [
  { key: 'name', label: 'Name', type: 'text' },
  { key: 'status', label: 'Status', type: 'select', options: [
    { value: 'active', label: 'Active' },
    { value: 'pending', label: 'Pending' },
  ]},
  { key: 'date', label: 'Date', type: 'date_range' },
];

function Demo() {
  const [activeFilters, setActiveFilters] = useState<ActiveFilter[]>([]);

  return (
    <CompositeFiltersInput
      filters={filters}
      value={activeFilters}
      onChange={setActiveFilters}
      disablePresets={false}
      disableHistory={false}
      storageKeyPrefix="my-app"
      placeholder="Filter... (try the menu)"
    />
  );
}

Built-in features (all persisted to localStorage):

  • Save presets - Name and store filter combinations
  • Favorites - Quick access to frequently used presets
  • History - Auto-tracked recent filter combinations

Styles API

CompositeFiltersInput supports Styles API, you can add styles to any inner element of the component with classNames prop.

Component Styles API

Hover over selectors to highlight corresponding elements

/*
 * Hover over selectors to apply outline styles
 *
 */

Styling Examples

Make it yours with styles or classNames:

Accent Border
Minimal Style
Pill Style
import { useState } from 'react';
import { CompositeFiltersInput } from 'mantine-composite-filters';
import type { ActiveFilter, FilterDefinition } from 'mantine-composite-filters';

const filters: FilterDefinition[] = [
  { key: 'search', label: 'Search', type: 'text' },
  { key: 'status', label: 'Status', type: 'select', options: [
    { value: 'active', label: 'Active' },
    { value: 'pending', label: 'Pending' },
  ]},
];

function Demo() {
  const [activeFilters, setActiveFilters] = useState<ActiveFilter[]>([]);

  return (
    <CompositeFiltersInput
      filters={filters}
      value={activeFilters}
      onChange={setActiveFilters}
      placeholder="Filter items..."
      styles={{
        container: {
          border: '2px solid var(--mantine-color-blue-5)',
          borderRadius: '12px',
        },
      }}
    />
  );
}

Inline Styles

// Direct object
<CompositeFiltersInput
  styles={{
    container: { borderRadius: '12px', borderColor: 'blue' },
  }}
/>

// Theme-aware function
<CompositeFiltersInput
  styles={(theme) => ({
    container: { backgroundColor: theme.colors.gray[0] },
  })}
/>

CSS Classes

<CompositeFiltersInput classNames={{ container: 'my-filter' }} />
.my-filter { border: 2px solid #228be6; }
.my-filter:focus-within { box-shadow: 0 0 0 2px rgba(34, 139, 230, 0.2); }

Custom Pills

Replace default filter chips with your own creative designs:

Add some filters to preview different pill styles

import { useState } from 'react';
import { Badge, ActionIcon } from '@mantine/core';
import { IconX } from '@tabler/icons-react';
import { CompositeFiltersInput } from 'mantine-composite-filters';
import type { ActiveFilter, FilterDefinition } from 'mantine-composite-filters';

const filters: FilterDefinition[] = [
  { key: 'name', label: 'Name', type: 'text' },
  { key: 'status', label: 'Status', type: 'select', options: [
    { value: 'active', label: 'Active' },
    { value: 'pending', label: 'Pending' },
  ]},
];

function Demo() {
  const [activeFilters, setActiveFilters] = useState<ActiveFilter[]>([]);

  return (
    <CompositeFiltersInput
      filters={filters}
      value={activeFilters}
      onChange={setActiveFilters}
      placeholder="Add filters..."
      renderPill={(filter, onRemove) => (
        <Badge
          size="lg"
          variant="gradient"
          gradient={{ from: 'violet', to: 'grape' }}
          rightSection={
            <ActionIcon size={16} radius="xl" variant="transparent" onClick={onRemove}>
              <IconX size={10} color="white" />
            </ActionIcon>
          }
        >
          {filter.label}: {filter.displayValue}
        </Badge>
      )}
    />
  );
}

Gradient Style

renderPill={(filter, onRemove) => (
  <Badge
    variant="gradient"
    gradient={{ from: 'violet', to: 'grape' }}
    rightSection={<CloseButton size="xs" onClick={onRemove} />}
  >
    {filter.label}: {filter.displayValue}
  </Badge>
)}

Outlined with Icon

renderPill={(filter, onRemove) => (
  <Paper withBorder radius="xl" px="sm" py={4}>
    <Group gap={6}>
      <ThemeIcon size={18} radius="xl" variant="light">
        <IconFilter size={10} />
      </ThemeIcon>
      <Text size="xs">{filter.label}</Text>
      <Text size="xs" c="blue" fw={600}>{filter.displayValue}</Text>
      <CloseButton size="xs" onClick={onRemove} />
    </Group>
  </Paper>
)}

Card Style with Color Accent

renderPill={(filter, onRemove) => (
  <Paper 
    shadow="xs" 
    px="sm" 
    py={6}
    style={{ borderLeft: '3px solid var(--mantine-color-blue-5)' }}
  >
    <Text size="xs" c="dimmed">{filter.label}</Text>
    <Text size="sm" fw={600}>{filter.displayValue}</Text>
    <CloseButton size="xs" onClick={onRemove} />
  </Paper>
)}

Keyboard Shortcuts

Power user? Navigate without touching your mouse:

import { useState } from 'react';
import { CompositeFiltersInput } from 'mantine-composite-filters';
import type { ActiveFilter, FilterDefinition } from 'mantine-composite-filters';

const filters: FilterDefinition[] = [
  { key: 'name', label: 'Name', type: 'text' },
  { key: 'email', label: 'Email', type: 'email' },
  { key: 'status', label: 'Status', type: 'select', options: [
    { value: 'active', label: 'Active' },
    { value: 'inactive', label: 'Inactive' },
  ]},
];

function Demo() {
  const [activeFilters, setActiveFilters] = useState<ActiveFilter[]>([]);

  return (
    <CompositeFiltersInput
      filters={filters}
      value={activeFilters}
      onChange={setActiveFilters}
      placeholder="Try keyboard shortcuts..."
    />
  );
}

// Shortcuts:
// ⌘/Ctrl + / - Focus input
// ⌘/Ctrl + ⌫ - Clear all
// Enter - Submit value
// Escape - Cancel
// Backspace - Remove last filter
ShortcutAction
Cmd/Ctrl + /Focus filter input
Cmd/Ctrl + BackspaceClear all filters
EnterConfirm value
EscapeCancel input
BackspaceRemove last filter

In Action

See it work with real data filtering:

Showing 5 of 5 users

UserRoleDepartmentStatus
AC

Alice Chen

alice@company.com

admin
Engineering
active
BS

Bob Smith

bob@company.com

editor
Design
active
CD

Carol Davis

carol@company.com

viewer
Marketing
inactive
DW

Dan Wilson

dan@company.com

editor
Sales
active
EM

Eva Martinez

eva@company.com

viewer
Engineering
active
import { useState, useMemo } from 'react';
import { Table } from '@mantine/core';
import { CompositeFiltersInput } from 'mantine-composite-filters';
import type { ActiveFilter, FilterDefinition } from 'mantine-composite-filters';

const filters: FilterDefinition[] = [
  { key: 'name', label: 'Name', type: 'text', operators: ['contains', '='] },
  { key: 'role', label: 'Role', type: 'select', options: [
    { value: 'admin', label: 'Admin' },
    { value: 'user', label: 'User' },
  ]},
  { key: 'status', label: 'Status', type: 'select', options: [
    { value: 'active', label: 'Active' },
    { value: 'inactive', label: 'Inactive' },
  ]},
];

const users = [
  { id: 1, name: 'Alice Chen', role: 'admin', status: 'active' },
  { id: 2, name: 'Bob Smith', role: 'user', status: 'active' },
  { id: 3, name: 'Carol Davis', role: 'user', status: 'inactive' },
];

function Demo() {
  const [activeFilters, setActiveFilters] = useState<ActiveFilter[]>([]);

  const filteredUsers = useMemo(() => {
    return users.filter(user => 
      activeFilters.every(filter => {
        const value = user[filter.key];
        if (filter.operator === 'contains') {
          return value.toLowerCase().includes(filter.value.toLowerCase());
        }
        return value.toLowerCase() === filter.value.toLowerCase();
      })
    );
  }, [activeFilters]);

  return (
    <>
      <CompositeFiltersInput
        filters={filters}
        value={activeFilters}
        onChange={setActiveFilters}
        placeholder="Filter users..."
      />
      <Table>
        <Table.Thead>
          <Table.Tr>
            <Table.Th>Name</Table.Th>
            <Table.Th>Role</Table.Th>
            <Table.Th>Status</Table.Th>
          </Table.Tr>
        </Table.Thead>
        <Table.Tbody>
          {filteredUsers.map(user => (
            <Table.Tr key={user.id}>
              <Table.Td>{user.name}</Table.Td>
              <Table.Td>{user.role}</Table.Td>
              <Table.Td>{user.status}</Table.Td>
            </Table.Tr>
          ))}
        </Table.Tbody>
      </Table>
    </>
  );
}

Pro Tips

Inline Editing - Click any part of an active filter to edit it directly (field, operator, or value).

Extend the Menu - Add your own actions:

<CompositeFiltersInput
  customActions={[
    { id: 'export', label: 'Export', onClick: handleExport },
    { id: 'share', label: 'Share Link', onClick: handleShare },
  ]}
/>

Isolate Storage - Use storageKeyPrefix when you have multiple filter instances:

<CompositeFiltersInput storageKeyPrefix="users-page" />
<CompositeFiltersInput storageKeyPrefix="orders-page" />

API Reference

FilterDefinition

{
  key: string;           // Unique ID
  label: string;         // Display name
  type: FilterType;      // text | email | number | select | multi_select | date | date_range
  options?: { value: string; label: string }[];  // For select types
  operators?: FilterOperator[];                   // Override defaults
  placeholder?: string;
  icon?: ReactNode;
}

ActiveFilter

{
  id: string;            // Auto-generated
  key: string;           // Matches FilterDefinition.key
  label: string;
  type: FilterType;
  operator: FilterOperator;
  value: string | string[] | [Date, Date];
  displayValue: string;  // Formatted for display
}

TypeScript

Fully typed. Import what you need:

import type { 
  FilterDefinition, 
  ActiveFilter, 
  FilterType,
  FilterOperator 
} from 'mantine-composite-filters';

Tips

  • Debounce - For large datasets, debounce the onChange before fetching
  • Server validation - Always validate filter values server-side
  • Mobile UX - Use overflowMode="wrap" on smaller screens
  • Unique keys - Each filter definition needs a unique key