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 { useRef, useCallback } from 'react';import { HotTable } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';
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 debounce = (callback, delay = 120) => { let timeoutId;
return (...args) => { window.clearTimeout(timeoutId); timeoutId = window.setTimeout(() => callback(...args), delay); };};
const ExampleComponent = () => { const hotRef = useRef(null);
const runSearch = useCallback( debounce((value) => { const hot = hotRef.current?.hotInstance;
if (!hot) { return; }
hot.getPlugin('search').query(value); hot.render(); }), [] );
function handleSearchInput(e) { runSearch(e.target.value); }
return ( <div> <div className="example-controls-container"> <div className="controls"> <label htmlFor="external-search-input">Search rows</label> <input id="external-search-input" type="search" placeholder="Type to highlight matching cells..." onInput={handleSearchInput} /> </div> </div> <HotTable ref={hotRef} data={data} rowHeaders={true} colHeaders={['Name', 'Team', 'Location', 'Email']} height="auto" width="100%" autoWrapRow={true} autoWrapCol={true} search={true} licenseKey="non-commercial-and-evaluation" /> </div> );};
export default ExampleComponent;import { useRef, useCallback } from 'react';import { HotTable, HotTableRef } from '@handsontable/react-wrapper';import { registerAllModules } from 'handsontable/registry';
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 debounce = (callback: (...args: unknown[]) => void, delay = 120) => { let timeoutId: ReturnType<typeof setTimeout>;
return (...args: unknown[]) => { window.clearTimeout(timeoutId); timeoutId = window.setTimeout(() => callback(...args), delay); };};
const ExampleComponent = () => { const hotRef = useRef<HotTableRef>(null);
const runSearch = useCallback( debounce((value: unknown) => { const hot = hotRef.current?.hotInstance;
if (!hot) { return; }
hot.getPlugin('search').query(String(value)); hot.render(); }), [] );
function handleSearchInput(e: React.FormEvent<HTMLInputElement>) { runSearch(e.currentTarget.value); }
return ( <div> <div className="example-controls-container"> <div className="controls"> <label htmlFor="external-search-input">Search rows</label> <input id="external-search-input" type="search" placeholder="Type to highlight matching cells..." onInput={handleSearchInput} /> </div> </div> <HotTable ref={hotRef} data={data} rowHeaders={true} colHeaders={['Name', 'Team', 'Location', 'Email']} height="auto" width="100%" autoWrapRow={true} autoWrapCol={true} search={true} licenseKey="non-commercial-and-evaluation" /> </div> );};
export default ExampleComponent;#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.