External search box
In this tutorial, you will add a search input outside Handsontable that highlights matching cells as you type. You will learn how to use the Search plugin’s query() method and hot.render() to apply real-time cell highlights from an external control.
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';
// Register all Handsontable modules.registerAllModules();
/* start:skip-in-preview */const data = [ ['Alice Johnson', 'Engineering', 'Berlin', 'alice.johnson@example.com'], ['Noah Smith', 'Design', 'Warsaw', 'noah.smith@example.com'], ['Mia Garcia', 'Marketing', 'New York', 'mia.garcia@example.com'], ['Liam Brown', 'Engineering', 'Toronto', 'liam.brown@example.com'], ['Emma Davis', 'Sales', 'London', 'emma.davis@example.com'], ['Oliver Miller', 'Support', 'Madrid', 'oliver.miller@example.com'],];/* end:skip-in-preview */
const exampleContainer = document.querySelector('#example1');
const searchWrapper = document.createElement('div');searchWrapper.className = 'example-controls-container';
const controlsDiv = document.createElement('div');controlsDiv.className = 'controls';
const searchLabel = document.createElement('label');searchLabel.setAttribute('for', 'external-search-input');searchLabel.textContent = 'Search rows';
const searchInput = document.createElement('input');searchInput.id = 'external-search-input';searchInput.type = 'search';searchInput.placeholder = 'Type to highlight matching cells...';
const hotContainer = document.createElement('div');
controlsDiv.appendChild(searchLabel);controlsDiv.appendChild(searchInput);searchWrapper.appendChild(controlsDiv);exampleContainer.appendChild(searchWrapper);exampleContainer.appendChild(hotContainer);
const hot = new Handsontable(hotContainer, { data, rowHeaders: true, colHeaders: ['Name', 'Team', 'Location', 'Email'], height: 'auto', width: '100%', autoWrapRow: true, autoWrapCol: true, search: true, licenseKey: 'non-commercial-and-evaluation',});
const debounce = (callback, delay = 120) => { let timeoutId;
return (...args) => { window.clearTimeout(timeoutId); timeoutId = window.setTimeout(() => callback(...args), delay); };};
const runSearch = debounce((value) => { const searchPlugin = hot.getPlugin('search');
searchPlugin.query(value); hot.render();});
searchInput.addEventListener('input', (event) => { runSearch(event.target.value);});import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';
// Register all Handsontable modules.registerAllModules();
/* start:skip-in-preview */const data = [ ['Alice Johnson', 'Engineering', 'Berlin', 'alice.johnson@example.com'], ['Noah Smith', 'Design', 'Warsaw', 'noah.smith@example.com'], ['Mia Garcia', 'Marketing', 'New York', 'mia.garcia@example.com'], ['Liam Brown', 'Engineering', 'Toronto', 'liam.brown@example.com'], ['Emma Davis', 'Sales', 'London', 'emma.davis@example.com'], ['Oliver Miller', 'Support', 'Madrid', 'oliver.miller@example.com'],];/* end:skip-in-preview */
const exampleContainer = document.querySelector('#example1');
if (!exampleContainer) { throw new Error('Example container not found.');}
const searchWrapper = document.createElement('div');searchWrapper.className = 'example-controls-container';
const controlsDiv = document.createElement('div');controlsDiv.className = 'controls';
const searchLabel = document.createElement('label');searchLabel.setAttribute('for', 'external-search-input');searchLabel.textContent = 'Search rows';
const searchInput = document.createElement('input');searchInput.id = 'external-search-input';searchInput.type = 'search';searchInput.placeholder = 'Type to highlight matching cells...';
const hotContainer = document.createElement('div');
controlsDiv.appendChild(searchLabel);controlsDiv.appendChild(searchInput);searchWrapper.appendChild(controlsDiv);exampleContainer.appendChild(searchWrapper);exampleContainer.appendChild(hotContainer);
const hot = new Handsontable(hotContainer, { data, rowHeaders: true, colHeaders: ['Name', 'Team', 'Location', 'Email'], height: 'auto', width: '100%', autoWrapRow: true, autoWrapCol: true, search: true, licenseKey: 'non-commercial-and-evaluation',});
const debounce = <T extends (...args: unknown[]) => void>(callback: T, delay = 120) => { let timeoutId: ReturnType<typeof setTimeout> | undefined;
return (...args: Parameters<T>) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => callback(...args), delay); };};
const runSearch = debounce((value: string) => { const searchPlugin = hot.getPlugin('search');
searchPlugin.query(value); hot.render();});
searchInput.addEventListener('input', (event) => { runSearch((event.target as HTMLInputElement).value);});#external-search-input { min-width: 20rem;}Enable the Search plugin
Set
search: truein grid settings:const hot = new Handsontable(container, {data,search: true,licenseKey: 'non-commercial-and-evaluation',});This enables the plugin and the default
htSearchResulthighlight class.Add an external input
Render a text input above the grid container:
const controls = document.createElement('div');const searchInput = document.createElement('input');searchInput.type = 'search';searchInput.placeholder = 'Search in table...';controls.appendChild(searchInput);container.parentElement?.insertBefore(controls, container);The input lives outside Handsontable, so you can style and place it like any other app control.
Bind the input to
query()Listen to input events, query the plugin, and re-render:
const searchPlugin = hot.getPlugin('search');searchInput.addEventListener('input', () => {searchPlugin.query(searchInput.value);hot.render();});query()updates each cell’sisSearchResultmetadata.hot.render()applies the updated highlight state.
Step 4 (optional): Debounce for large datasets
If you search very large tables, debounce the input callback to reduce render frequency:
function debounce<T extends (...args: any[]) => void>(callback: T, wait = 120) { let timeoutId: ReturnType<typeof setTimeout> | undefined;
return (...args: Parameters<T>) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => callback(...args), wait); };}Use this wrapper around your search handler when needed.
What you learned
- How to enable the
Searchplugin withsearch: truein Handsontable settings. - How to place a search input outside the grid and call
hot.getPlugin('search').query(value)on every input event. - Why you must call
hot.render()afterquery()to apply the updatedisSearchResultmetadata to cells. - How to add debouncing to limit render frequency when searching large datasets.
Next steps
- Explore highlight search matches to wrap matched text in
<mark>tags instead of using the default cell highlight class. - Add multi-column filtering to let users filter by multiple columns at once through an external panel.