Multi-column filter panel
In this tutorial, you will build an external filter panel with a category dropdown and a price range slider that controls Handsontable filtering. You will learn how to apply multiple conditions at once through the Filters plugin API and clear them all with a single button.
import { useRef } from 'react';import { HotTable } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';import './example1.css';
registerAllModules();
/* start:skip-in-preview */const sourceData = [ { name: 'Trail Bike', category: 'Bikes', price: 1499, stock: 12 }, { name: 'Road Helmet', category: 'Safety', price: 89, stock: 42 }, { name: 'Flat Pedals', category: 'Components', price: 59, stock: 80 }, { name: 'Hydration Pack', category: 'Accessories', price: 129, stock: 23 }, { name: 'Brake Pads', category: 'Components', price: 25, stock: 150 }, { name: 'Cycling Glasses', category: 'Accessories', price: 79, stock: 33 }, { name: 'Chain Lube', category: 'Maintenance', price: 16, stock: 99 }, { name: 'Torque Wrench', category: 'Maintenance', price: 139, stock: 14 }, { name: 'Kids Helmet', category: 'Safety', price: 54, stock: 20 }, { name: 'Gravel Bike', category: 'Bikes', price: 2199, stock: 7 },];/* end:skip-in-preview */
const FilterLabel = ({ label, children }) => ( <label className="filter-label"> {label} {children} </label>);
const ExampleComponent = () => { const hotRef = useRef(null); const nameRef = useRef(null); const categoryRef = useRef(null); const minPriceRef = useRef(null); const maxPriceRef = useRef(null);
const applyFilters = () => { const hot = hotRef.current?.hotInstance;
if (!hot) { return; }
const name = nameRef.current?.value ?? ''; const category = categoryRef.current?.value ?? ''; const minPrice = minPriceRef.current?.value ?? ''; const maxPrice = maxPriceRef.current?.value ?? ''; const filtersPlugin = hot.getPlugin('filters');
filtersPlugin.clearConditions();
if (category) { filtersPlugin.addCondition(1, 'eq', [category]); }
if (name.trim()) { filtersPlugin.addCondition(0, 'contains', [name.trim()]); }
if (minPrice && maxPrice) { const lowerBound = Number(minPrice); const upperBound = Number(maxPrice);
if (Number.isFinite(lowerBound) && Number.isFinite(upperBound)) { filtersPlugin.addCondition(2, 'between', [lowerBound, upperBound]); } } else if (minPrice) { const lowerBound = Number(minPrice);
if (Number.isFinite(lowerBound)) { filtersPlugin.addCondition(2, 'gte', [lowerBound]); } } else if (maxPrice) { const upperBound = Number(maxPrice);
if (Number.isFinite(upperBound)) { filtersPlugin.addCondition(2, 'lte', [upperBound]); } }
filtersPlugin.filter(); };
const clearFilters = () => { if (nameRef.current) nameRef.current.value = ''; if (categoryRef.current) categoryRef.current.value = ''; if (minPriceRef.current) minPriceRef.current.value = ''; if (maxPriceRef.current) maxPriceRef.current.value = '';
applyFilters(); };
return ( <div> <div className="example-controls-container"> <div className="filter-panel"> <FilterLabel label="Product name"> <input ref={nameRef} type="text" placeholder="Contains..." onChange={applyFilters} /> </FilterLabel> <FilterLabel label="Category"> <select ref={categoryRef} onChange={applyFilters}> <option value="">All categories</option> <option value="Bikes">Bikes</option> <option value="Safety">Safety</option> <option value="Components">Components</option> <option value="Accessories">Accessories</option> <option value="Maintenance">Maintenance</option> </select> </FilterLabel> <FilterLabel label="Min price"> <input ref={minPriceRef} type="number" min="0" placeholder="0" onChange={applyFilters} /> </FilterLabel> <FilterLabel label="Max price"> <input ref={maxPriceRef} type="number" min="0" placeholder="2500" onChange={applyFilters} /> </FilterLabel> <button type="button" onClick={clearFilters}> Clear all filters </button> </div> </div> <HotTable ref={hotRef} data={sourceData} columns={[ { data: 'name', type: 'text', title: 'Product' }, { data: 'category', type: 'text', title: 'Category' }, { data: 'price', type: 'numeric', title: 'Price' }, { data: 'stock', type: 'numeric', title: 'Stock' }, ]} colHeaders={['Product', 'Category', 'Price', 'Stock']} rowHeaders={true} filters={true} dropdownMenu={false} width="100%" height={320} autoWrapRow={true} autoWrapCol={true} licenseKey="non-commercial-and-evaluation" /> </div> );};
export default ExampleComponent;import { useRef, type ReactNode } from 'react';import { HotTable, HotTableRef } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';import './example1.css';
registerAllModules();
/* start:skip-in-preview */const sourceData = [ { name: 'Trail Bike', category: 'Bikes', price: 1499, stock: 12 }, { name: 'Road Helmet', category: 'Safety', price: 89, stock: 42 }, { name: 'Flat Pedals', category: 'Components', price: 59, stock: 80 }, { name: 'Hydration Pack', category: 'Accessories', price: 129, stock: 23 }, { name: 'Brake Pads', category: 'Components', price: 25, stock: 150 }, { name: 'Cycling Glasses', category: 'Accessories', price: 79, stock: 33 }, { name: 'Chain Lube', category: 'Maintenance', price: 16, stock: 99 }, { name: 'Torque Wrench', category: 'Maintenance', price: 139, stock: 14 }, { name: 'Kids Helmet', category: 'Safety', price: 54, stock: 20 }, { name: 'Gravel Bike', category: 'Bikes', price: 2199, stock: 7 },];/* end:skip-in-preview */
type FilterLabelProps = { label: string; children: ReactNode;};
const FilterLabel = ({ label, children }: FilterLabelProps) => ( <label className="filter-label"> {label} {children} </label>);
const ExampleComponent = () => { const hotRef = useRef<HotTableRef>(null); const nameRef = useRef<HTMLInputElement>(null); const categoryRef = useRef<HTMLSelectElement>(null); const minPriceRef = useRef<HTMLInputElement>(null); const maxPriceRef = useRef<HTMLInputElement>(null);
const applyFilters = () => { const hot = hotRef.current?.hotInstance;
if (!hot) { return; }
const name = nameRef.current?.value ?? ''; const category = categoryRef.current?.value ?? ''; const minPrice = minPriceRef.current?.value ?? ''; const maxPrice = maxPriceRef.current?.value ?? ''; const filtersPlugin = hot.getPlugin('filters');
filtersPlugin.clearConditions();
if (category) { filtersPlugin.addCondition(1, 'eq', [category]); }
if (name.trim()) { filtersPlugin.addCondition(0, 'contains', [name.trim()]); }
if (minPrice && maxPrice) { const lowerBound = Number(minPrice); const upperBound = Number(maxPrice);
if (Number.isFinite(lowerBound) && Number.isFinite(upperBound)) { filtersPlugin.addCondition(2, 'between', [lowerBound, upperBound]); } } else if (minPrice) { const lowerBound = Number(minPrice);
if (Number.isFinite(lowerBound)) { filtersPlugin.addCondition(2, 'gte', [lowerBound]); } } else if (maxPrice) { const upperBound = Number(maxPrice);
if (Number.isFinite(upperBound)) { filtersPlugin.addCondition(2, 'lte', [upperBound]); } }
filtersPlugin.filter(); };
const clearFilters = () => { if (nameRef.current) nameRef.current.value = ''; if (categoryRef.current) categoryRef.current.value = ''; if (minPriceRef.current) minPriceRef.current.value = ''; if (maxPriceRef.current) maxPriceRef.current.value = '';
applyFilters(); };
return ( <div> <div className="example-controls-container"> <div className="filter-panel"> <FilterLabel label="Product name"> <input ref={nameRef} type="text" placeholder="Contains..." onChange={applyFilters} /> </FilterLabel> <FilterLabel label="Category"> <select ref={categoryRef} onChange={applyFilters}> <option value="">All categories</option> <option value="Bikes">Bikes</option> <option value="Safety">Safety</option> <option value="Components">Components</option> <option value="Accessories">Accessories</option> <option value="Maintenance">Maintenance</option> </select> </FilterLabel> <FilterLabel label="Min price"> <input ref={minPriceRef} type="number" min="0" placeholder="0" onChange={applyFilters} /> </FilterLabel> <FilterLabel label="Max price"> <input ref={maxPriceRef} type="number" min="0" placeholder="2500" onChange={applyFilters} /> </FilterLabel> <button type="button" onClick={clearFilters}> Clear all filters </button> </div> </div> <HotTable ref={hotRef} data={sourceData} columns={[ { data: 'name', type: 'text', title: 'Product' }, { data: 'category', type: 'text', title: 'Category' }, { data: 'price', type: 'numeric', title: 'Price' }, { data: 'stock', type: 'numeric', title: 'Stock' }, ]} colHeaders={['Product', 'Category', 'Price', 'Stock']} rowHeaders={true} filters={true} dropdownMenu={false} width="100%" height={320} autoWrapRow={true} autoWrapCol={true} licenseKey="non-commercial-and-evaluation" /> </div> );};
export default ExampleComponent;.filter-panel { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: flex-end;}
/* Stack label text above the control (overrides the inline-flex horizontal layout from interactive-example.css) */.example-controls-container .filter-label { display: flex; flex-direction: column; align-items: flex-start; gap: 0.25rem; min-width: 120px;}Overview
This recipe shows how to control the Filters plugin from a filter panel outside of the grid. The panel includes controls aligned with the grid columns, and it applies all active filters together with AND logic.
Difficulty: Intermediate Time: ~20 minutes Libraries: Handsontable only
What You’ll Build
An external filter panel that:
- Enables
filters: truewhile hiding the built-in filter menu UI. - Uses
hot.getPlugin('filters')to control filtering through API calls. - Applies a text filter with
addCondition(columnIndex, 'contains', [value]). - Applies a numeric range filter with
addCondition(columnIndex, 'between', [min, max]). - Clears and re-applies all active conditions every time controls change.
- Includes a Clear all filters button that restores all rows.
Step 1 - Enable filtering and create the grid
Enable the Filters plugin in Handsontable configuration:
const hot = new Handsontable(container, { data: productData, colHeaders: ['Product', 'Category', 'Price', 'Stock'], columns: [ { data: 'name' }, { data: 'category' }, { data: 'price', type: 'numeric', numericFormat: { pattern: '$0,0.00', culture: 'en-US' } }, { data: 'stock', type: 'numeric' }, ], filters: true, dropdownMenu: false, licenseKey: 'non-commercial-and-evaluation',});Setting dropdownMenu: false keeps the filter panel external, while the Filters plugin remains active.
Step 2 - Get the plugin and apply conditions
Get the plugin instance and apply conditions every time a filter value changes:
const filtersPlugin = hot.getPlugin('filters');
const applyFilters = () => { filtersPlugin.clearConditions();
if (enteredName) { filtersPlugin.addCondition(0, 'contains', [enteredName]); }
if (selectedCategory) { filtersPlugin.addCondition(1, 'eq', [selectedCategory]); }
if (minPrice && maxPrice) { filtersPlugin.addCondition(2, 'between', [Number(minPrice), Number(maxPrice)]); } else if (minPrice) { filtersPlugin.addCondition(2, 'gte', [Number(minPrice)]); } else if (maxPrice) { filtersPlugin.addCondition(2, 'lte', [Number(maxPrice)]); }
filtersPlugin.filter(); hot.render();};This pattern guarantees each update uses the current set of active controls.
Step 3 - Add clear-all behavior
Add a button that clears control values, removes all conditions, and shows all rows again:
clearAllButton.addEventListener('click', () => { nameInput.value = ''; categorySelect.value = ''; minPriceInput.value = ''; maxPriceInput.value = ''; filtersPlugin.clearConditions(); filtersPlugin.filter(); hot.render();});How it works
- User changes one or more controls in the external panel.
- The code clears previously applied conditions.
- The code re-applies current conditions for product name, category, and price.
filtersPlugin.filter()updates the visible rows.- Clear all filters resets controls and restores the full dataset.
The full implementation is available in the runnable example above.
What you learned
- How to use
filtersPlugin.clearConditions()andfiltersPlugin.addCondition()to apply fresh conditions on every control change. - How to call
filtersPlugin.filter()to update the visible rows after adding conditions, and howhot.render()keeps the view in sync. - How to build a clear-all button that resets external controls, removes all conditions, and restores the full dataset.
- Why you must enable the
Filtersplugin withfilters: trueand pair it withdropdownMenu: trueto expose per-column filter UI alongside your external panel.
Next steps
- Explore external search box to add a text search that works alongside the filter panel.
- Read the Filters plugin API reference for the full list of built-in condition types (between, contains, begins with, and more).