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

Peer Dependencies

This package requires the following peer dependencies:

yarn add @mantine/core @mantine/hooks @mantine/dates @mantine/notifications @tabler/icons-react

Make sure you have Mantine properly set up in your project. See Mantine Getting Started for setup instructions.

Required setup:

  • MantineProvider wrapping your app
  • Notifications component for toast notifications
  • Import mantine-composite-filters/styles.css in your app
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import '@mantine/notifications/styles.css';
import 'mantine-composite-filters/styles.css';

import { MantineProvider } from '@mantine/core';
import { Notifications } from '@mantine/notifications';

function App() {
  return (
    <MantineProvider>
      <Notifications />
      {/* Your app */}
    </MantineProvider>
  );
}

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
  • Flexible storage with localStorage default or custom adapters

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 by default):

  • Save presets - Click "Save as preset" to open a modal where you can name and store filter combinations
  • Favorites - Star presets for quick access
  • History - Auto-tracked recent filter combinations (last 10)

Custom Save Preset Modal

You can provide your own modal for saving presets using the renderSavePresetModal prop:

import { Modal, TextInput, Button, Stack } from '@mantine/core';
import { useState } from 'react';
import type { SavePresetModalProps } from 'mantine-composite-filters';

function MyCustomSaveModal({ opened, onClose, onSave, activeFilters }: SavePresetModalProps) {
  const [name, setName] = useState('');

  const handleSave = () => {
    if (name.trim()) {
      onSave(name.trim());
      setName('');
    }
  };

  return (
    <Modal opened={opened} onClose={onClose} title="Save Your Filters">
      <Stack>
        <TextInput
          label="Preset Name"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="My awesome filter preset"
        />
        <p>You're saving {activeFilters.length} filters</p>
        <Button onClick={handleSave}>Save Preset</Button>
      </Stack>
    </Modal>
  );
}

// Usage
<CompositeFiltersInput
  filters={filters}
  value={activeFilters}
  onChange={setActiveFilters}
  renderSavePresetModal={(props) => <MyCustomSaveModal {...props} />}
/>

The SavePresetModalProps includes:

  • opened - Whether the modal should be visible
  • onClose - Function to close the modal
  • onSave - Function to call with the preset name when saving
  • activeFilters - Current active filters being saved

Custom Storage Adapters

Need to store presets on your server or use a different storage mechanism? Create custom storage adapters:

Data is lost on page refresh

Storage Adapter Interface:

interface StorageAdapter<T> {
  get: () => T | Promise<T>;
  set: (value: T) => void | Promise<void>;
  remove: () => void | Promise<void>;
}
import { useState, useMemo } from 'react';
import { 
  CompositeFiltersInput,
  createLocalStorageAdapter,
  type ActiveFilter,
  type FilterDefinition,
  type StorageAdapter,
  type SavedFilterPreset,
  type FilterHistory,
} 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' },
  ]},
];

// Example: In-memory adapter (no persistence)
const createInMemoryAdapter = <T,>(defaultValue: T): StorageAdapter<T> => {
  let data: T = defaultValue;
  return {
    get: () => data,
    set: (value: T) => { data = value; },
    remove: () => { data = defaultValue; },
  };
};

// Example: API-based adapter (async)
const createApiAdapter = <T,>(endpoint: string): StorageAdapter<T> => ({
  get: async () => {
    const response = await fetch(endpoint);
    return response.json();
  },
  set: async (value: T) => {
    await fetch(endpoint, {
      method: 'POST',
      body: JSON.stringify(value),
    });
  },
  remove: async () => {
    await fetch(endpoint, { method: 'DELETE' });
  },
});

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

  const presetsAdapter = useMemo(() => 
    createInMemoryAdapter<SavedFilterPreset[]>([]), []
  );
  const historyAdapter = useMemo(() => 
    createInMemoryAdapter<FilterHistory[]>([]), []
  );

  return (
    <CompositeFiltersInput
      filters={filters}
      value={activeFilters}
      onChange={setActiveFilters}
      presetsStorageAdapter={presetsAdapter}
      historyStorageAdapter={historyAdapter}
      placeholder="Using custom storage..."
    />
  );
}

StorageAdapter Interface

interface StorageAdapter<T> {
  get: () => T | Promise<T>;
  set: (value: T) => void | Promise<void>;
  remove: () => void | Promise<void>;
}

Built-in Adapters

The package exports createLocalStorageAdapter for creating localStorage adapters with proper Date serialization:

import { createLocalStorageAdapter } from 'mantine-composite-filters';

const presetsAdapter = createLocalStorageAdapter<SavedFilterPreset[]>(
  'my-presets-key',
  [] // default value
);

API Storage Example

Store presets on your server:

import type { StorageAdapter, SavedFilterPreset } from 'mantine-composite-filters';

const apiPresetsAdapter: StorageAdapter<SavedFilterPreset[]> = {
  get: async () => {
    const response = await fetch('/api/filter-presets');
    return response.json();
  },
  set: async (presets) => {
    await fetch('/api/filter-presets', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(presets),
    });
  },
  remove: async () => {
    await fetch('/api/filter-presets', { method: 'DELETE' });
  },
};

<CompositeFiltersInput
  filters={filters}
  value={activeFilters}
  onChange={setActiveFilters}
  presetsStorageAdapter={apiPresetsAdapter}
  historyStorageAdapter={apiHistoryAdapter}
/>

useCompositeFilters Hook

The useCompositeFilters hook provides a complete state management solution for filters, presets, and history - all in one hook with 70+ helper functions.

Clean
0 filters
0 presets
0 history
import { 
  CompositeFiltersInput, 
  useCompositeFilters,
  type FilterDefinition 
} from 'mantine-composite-filters';

const filterDefinitions: FilterDefinition[] = [
  { key: 'name', label: 'Name', type: 'text' },
  { key: 'status', label: 'Status', type: 'select', options: [...] },
];

function Demo() {
  const {
    activeFilters,
    setActiveFilters,
    addFilterByKey,
    removeFilter,
    clearFilters,
    filtersCount,
    hasAnyFilter,
    toQueryString,
    toApiFormat,
    isDirty,
    presets,
    savePreset,
    history,
  } = useCompositeFilters({
    filterDefinitions,
    enableHistory: true,
    enablePresets: true,
  });

  return (
    <>
      <CompositeFiltersInput
        filters={filterDefinitions}
        value={activeFilters}
        onChange={setActiveFilters}
        disablePresets
        disableHistory
      />
      
      <Button onClick={() => addFilterByKey('status', '=', 'active')}>
        Add Active Status
      </Button>
      <Button onClick={clearFilters}>Clear All</Button>
      
      <Text>Query: {toQueryString()}</Text>
      <Text>Filters: {filtersCount}</Text>
    </>
  );
}

Basic Usage

import { useCompositeFilters, CompositeFiltersInput } from 'mantine-composite-filters';

const filterDefinitions = [
  { key: 'status', label: 'Status', type: 'select', options: [...] },
  { key: 'name', label: 'Name', type: 'text' },
];

function MyComponent() {
  const {
    activeFilters,
    setActiveFilters,
    addFilterByKey,
    removeFilter,
    clearFilters,
    filtersCount,
    presets,
    savePreset,
    loadPreset,
    history,
  } = useCompositeFilters({
    filterDefinitions,
    initialFilters: [],
    onFiltersChange: (filters) => console.log('Filters changed:', filters),
  });

  return (
    <CompositeFiltersInput
      filters={filterDefinitions}
      value={activeFilters}
      onChange={setActiveFilters}
    />
  );
}

Hook Options

interface UseCompositeFiltersOptions {
  filterDefinitions: FilterDefinition[];
  initialFilters?: ActiveFilter[];
  onFiltersChange?: (filters: ActiveFilter[]) => void;
  storageKey?: string;
  storageAdapter?: StorageAdapter<ActiveFilter[]>;
  presetsStorageKey?: string;
  presetsStorageAdapter?: StorageAdapter<SavedFilterPreset[]>;
  historyStorageKey?: string;
  historyStorageAdapter?: StorageAdapter<FilterHistory[]>;
  maxHistory?: number;
  enableHistory?: boolean;
  enablePresets?: boolean;
}

Available Functions

Filter CRUD Operations:

FunctionDescription
addFilter(filter)Add a filter and return its ID
addFilterByKey(key, operator, value, displayValue?)Add filter using definition key
removeFilter(id)Remove filter by ID
removeFilterByKey(key)Remove all filters with key
removeFiltersByKeys(keys)Remove filters matching any key
updateFilter(id, updates)Update filter properties
updateFilterValue(id, value, displayValue?)Update just the value
updateFilterOperator(id, operator)Update just the operator
replaceFilter(id, newFilter)Replace entire filter
clearFilters()Remove all filters
resetFilters()Reset to initial filters

Filter Getters:

FunctionDescription
getFilterById(id)Get filter by ID
getFilterByKey(key)Get first filter with key
getFiltersByKey(key)Get all filters with key
getFiltersByType(type)Get filters by type
getFiltersByOperator(operator)Get filters by operator
getFirstFilter()Get first filter
getLastFilter()Get last filter

Filter Checks:

Function/PropertyDescription
hasFilter(key)Check if filter exists
hasFilterById(id)Check by ID
hasFilterWithValue(key, value)Check key + value combo
hasAnyFilter()Any filters active?
hasMultipleFilters()More than one filter?
filtersCountNumber of active filters

Reordering:

FunctionDescription
moveFilterUp(id)Move filter up
moveFilterDown(id)Move filter down
moveFilterToStart(id)Move to first position
moveFilterToEnd(id)Move to last position
swapFilters(id1, id2)Swap two filters
reorderFilters(ids)Reorder by ID array

Utilities:

FunctionDescription
duplicateFilter(id)Clone a filter
toggleFilter(key, operator, value)Add or remove filter
upsertFilter(key, operator, value)Update or create filter

Serialization:

FunctionDescription
toQueryParams()Convert to URL params object
toQueryString()Convert to URL query string
toApiFormat()Convert to API-friendly format
toJSON()Export as JSON string
fromJSON(json)Import from JSON
serialize()Base64 encode filters
deserialize(data)Base64 decode filters

Presets:

Function/PropertyDescription
presetsAll saved presets
sortedPresetsPresets sorted by favorite
favoritePresetsOnly favorite presets
presetsCountNumber of presets
hasPresetsAny presets saved?
savePreset(name)Save current filters
loadPreset(preset)Load a preset
loadPresetById(id)Load preset by ID
deletePreset(id)Delete a preset
updatePreset(id, updates)Update preset
renamePreset(id, newName)Rename preset
togglePresetFavorite(id)Toggle favorite
duplicatePreset(id, newName?)Clone preset
overwritePreset(id)Overwrite with current
getPresetById(id)Get preset by ID
clearPresets()Delete all presets

History:

Function/PropertyDescription
historyFull history array
recentHistoryLast 5 entries
historyCountNumber of entries
hasHistoryAny history?
loadFromHistory(item)Load history item
loadFromHistoryByIndex(index)Load by index
clearHistory()Clear all history
removeHistoryItem(timestamp)Remove entry
undoToLastHistory()Undo to previous

Definitions:

FunctionDescription
getDefinition(key)Get filter definition
getAvailableDefinitions()Unused definitions
isDefinitionUsed(key)Is definition in use?
canAddMoreFilters(max?)Can add more filters?

State Tracking:

Function/PropertyDescription
isDirtyFilters changed from initial?
markAsClean()Mark current state as clean
hasUnsavedChanges()Same as isDirty

Examples

Programmatic Filter Management:

const { addFilterByKey, removeFilterByKey, clearFilters } = useCompositeFilters({
  filterDefinitions,
});

// Add a filter programmatically
addFilterByKey('status', '=', 'active', 'Active');

// Remove all status filters
removeFilterByKey('status');

// Clear everything
clearFilters();

URL Sync:

const { toQueryString, fromJSON, toJSON } = useCompositeFilters({
  filterDefinitions,
});

// Sync to URL
useEffect(() => {
  const params = toQueryString();
  window.history.replaceState({}, '', `?${params}`);
}, [toQueryString]);

// Share filters
const shareFilters = () => {
  const encoded = btoa(toJSON());
  navigator.clipboard.writeText(`${window.location.origin}?filters=${encoded}`);
};

API Integration:

const { toApiFormat, activeFilters } = useCompositeFilters({
  filterDefinitions,
  onFiltersChange: (filters) => {
    // Debounce and fetch
    fetchData(toApiFormat());
  },
});

Dirty State Tracking:

const { isDirty, markAsClean, resetFilters } = useCompositeFilters({
  filterDefinitions,
  initialFilters: savedFilters,
});

const handleSave = () => {
  saveToServer(activeFilters);
  markAsClean();
};

// Show unsaved changes warning
{isDirty && <Text c="yellow">You have unsaved changes</Text>}

Using Individual Hooks

For more granular control, you can also use the individual hooks:

import { useFilterPresets, useFilterHistory } from 'mantine-composite-filters';

const { presets, savePreset } = useFilterPresets({
  storageAdapter: myCustomAdapter,
  onLoad: setActiveFilters,
});

const { history } = useFilterHistory(activeFilters, {
  storageAdapter: myHistoryAdapter,
});

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,
  FilterValue,
  StorageAdapter,
  SavedFilterPreset,
  FilterHistory,
  SavePresetModalProps,
  UseCompositeFiltersOptions,
  UseCompositeFiltersReturn,
} 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