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 Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';
// Register all Handsontable's modules.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 rootContainer = document.querySelector('#example1');
rootContainer.innerHTML = ` <div class="example-controls-container"> <div class="filter-panel"> <label class="filter-label filter-label--wide"> Product name <input id="nameFilter" type="text" placeholder="Contains..." /> </label> <label class="filter-label filter-label--wide"> Category <select id="categoryFilter"> <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> </label> <label class="filter-label"> Min price <input id="minPriceFilter" type="number" min="0" placeholder="0" /> </label> <label class="filter-label"> Max price <input id="maxPriceFilter" type="number" min="0" placeholder="2500" /> </label> <button id="clearFilters" type="button">Clear all filters</button> </div> </div> <div id="hot"></div>`;
const container = rootContainer.querySelector('#hot');
const hot = new Handsontable(container, { 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',});
const filtersPlugin = hot.getPlugin('filters');
const categoryFilter = rootContainer.querySelector('#categoryFilter');const nameFilter = rootContainer.querySelector('#nameFilter');const minPriceFilter = rootContainer.querySelector('#minPriceFilter');const maxPriceFilter = rootContainer.querySelector('#maxPriceFilter');const clearFiltersButton = rootContainer.querySelector('#clearFilters');
function applyFilters() { const selectedCategory = categoryFilter.value; const enteredName = nameFilter.value.trim(); const minPrice = minPriceFilter.value.trim(); const maxPrice = maxPriceFilter.value.trim();
filtersPlugin.clearConditions();
if (selectedCategory) { filtersPlugin.addCondition(1, 'eq', [selectedCategory]); }
if (enteredName) { filtersPlugin.addCondition(0, 'contains', [enteredName]); }
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(); hot.render();}
function clearFilters() { categoryFilter.value = ''; nameFilter.value = ''; minPriceFilter.value = ''; maxPriceFilter.value = '';
filtersPlugin.clearConditions(); filtersPlugin.filter(); hot.render();}
categoryFilter.addEventListener('change', applyFilters);nameFilter.addEventListener('input', applyFilters);minPriceFilter.addEventListener('input', applyFilters);maxPriceFilter.addEventListener('input', applyFilters);clearFiltersButton.addEventListener('click', clearFilters);import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';
// Register all Handsontable's modules.registerAllModules();
type Product = { name: string; category: string; price: number; stock: number;};
/* start:skip-in-preview */const sourceData: Product[] = [ { 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 rootContainer = document.querySelector('#example1')!;
rootContainer.innerHTML = ` <div class="example-controls-container"> <div class="filter-panel"> <label class="filter-label filter-label--wide"> Product name <input id="nameFilter" type="text" placeholder="Contains..." /> </label> <label class="filter-label filter-label--wide"> Category <select id="categoryFilter"> <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> </label> <label class="filter-label"> Min price <input id="minPriceFilter" type="number" min="0" placeholder="0" /> </label> <label class="filter-label"> Max price <input id="maxPriceFilter" type="number" min="0" placeholder="2500" /> </label> <button id="clearFilters" type="button">Clear all filters</button> </div> </div> <div id="hot"></div>`;
const container = rootContainer.querySelector('#hot')!;
const hot = new Handsontable(container, { 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',});
const filtersPlugin = hot.getPlugin('filters');
const categoryFilter = rootContainer.querySelector('#categoryFilter') as HTMLSelectElement;const nameFilter = rootContainer.querySelector('#nameFilter') as HTMLInputElement;const minPriceFilter = rootContainer.querySelector('#minPriceFilter') as HTMLInputElement;const maxPriceFilter = rootContainer.querySelector('#maxPriceFilter') as HTMLInputElement;const clearFiltersButton = rootContainer.querySelector('#clearFilters') as HTMLButtonElement;
function applyFilters(): void { const selectedCategory = categoryFilter.value; const enteredName = nameFilter.value.trim(); const minPrice = minPriceFilter.value.trim(); const maxPrice = maxPriceFilter.value.trim();
filtersPlugin.clearConditions();
if (selectedCategory) { filtersPlugin.addCondition(1, 'eq', [selectedCategory]); }
if (enteredName) { filtersPlugin.addCondition(0, 'contains', [enteredName]); }
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(); hot.render();}
function clearFilters(): void { categoryFilter.value = ''; nameFilter.value = ''; minPriceFilter.value = ''; maxPriceFilter.value = '';
filtersPlugin.clearConditions(); filtersPlugin.filter(); hot.render();}
categoryFilter.addEventListener('change', applyFilters);nameFilter.addEventListener('input', applyFilters);minPriceFilter.addEventListener('input', applyFilters);maxPriceFilter.addEventListener('input', applyFilters);clearFiltersButton.addEventListener('click', clearFilters);.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;}
.example-controls-container .filter-label--wide { min-width: 160px;}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).