Skip to main content
RapidDev - Software Development Agency
retool-tutorial

How to Implement Search Functionality in Retool

Implement search in Retool by adding a Text Input named searchInput and using {{ searchInput.value }} in a SQL ILIKE query. Add a Change event handler with 300ms debounce to trigger the query. For instant client-side filtering, use a transformer with Array.filter(). The Table component also has a built-in search bar you can enable with one click in the Inspector.

What you'll learn

  • Create a Text Input search bar with a debounced Change handler that triggers a filtered SQL query
  • Write SQL ILIKE queries using {{ searchInput.value }} for case-insensitive server-side search
  • Filter query results client-side in a transformer using Array.filter() and .includes()
  • Enable and configure the Table component's built-in search bar for instant no-code filtering
  • Combine search with other filters (dropdowns, date pickers) using AND clauses
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read15-20 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
TL;DR

Implement search in Retool by adding a Text Input named searchInput and using {{ searchInput.value }} in a SQL ILIKE query. Add a Change event handler with 300ms debounce to trigger the query. For instant client-side filtering, use a transformer with Array.filter(). The Table component also has a built-in search bar you can enable with one click in the Inspector.

Quick facts about this guide
FactValue
ToolRetool
DifficultyBeginner
Time required15-20 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 2026

Three Patterns for App-Wide Search in Retool

Search in Retool can be implemented at three levels: built-in table search (zero-code, client-side), client-side transformer filtering (fast, no extra queries), and server-side ILIKE queries (scalable, searches the full dataset). Choosing the right level depends on data volume and search scope.

Built-in Table search filters the rows already loaded in the table component. It requires no configuration and works instantly — but only searches what is currently in the component, not the full database. Client-side transformer filtering is similar but gives you more control over the filter logic and can work across multiple tables. Server-side ILIKE queries run a new database query for each search, which enables searching millions of rows but requires debouncing.

This tutorial builds all three patterns and adds multi-field search across name, email, and company columns as a practical example. It also shows combining search with status filter dropdowns using AND clauses.

Prerequisites

  • A Retool app with a Table component displaying data from a query
  • A PostgreSQL or MySQL database resource configured
  • Basic familiarity with SQL WHERE clauses and Retool event handlers

Step-by-step guide

1

Enable the Table component's built-in search bar

The simplest search implementation: select the Table component, go to Inspector → General → Search, and enable the 'Show search bar' toggle. A search input appears above the table header. Users type in it and the table filters rows client-side in real time. This searches across all visible string columns automatically. No queries, no event handlers, no configuration. Limitation: only searches currently loaded rows, not the full database.

Expected result: A search bar appears above the table. Typing in it immediately filters the displayed rows without re-querying the database.

2

Add a Text Input search bar with a debounced query trigger

For server-side search across the full dataset, add a Text Input named searchInput above the table. Set Placeholder to 'Search by name, email, or company…'. In Inspector → Interaction → Event Handlers, add a Change event that triggers your main data query (e.g., getCustomers). Enable Debounce in the event handler's Advanced Options and set 300ms. This waits for the user to pause typing before sending the query, reducing database load.

Expected result: Typing in searchInput triggers the database query 300ms after the user pauses. The table updates with matching results.

3

Write a multi-field ILIKE query for server-side search

Create (or modify) the data-fetching SQL query to filter based on {{ searchInput.value }}. Use ILIKE for case-insensitive matching and the % wildcard for substring search. Use OR clauses to search across multiple fields simultaneously. Handle the empty-search case (show all records when the search box is empty) with a conditional expression.

typescript
1-- SQL query: getCustomers
2-- Run on page load: ON
3-- Trigger: searchInput onChange (debounced 300ms)
4
5SELECT
6 c.id,
7 c.full_name,
8 c.email,
9 c.company_name,
10 c.status,
11 c.created_at
12FROM customers c
13WHERE
14 -- When search is empty, return all records; when not empty, filter:
15 ({{ searchInput.value === '' || searchInput.value === null }}
16 OR c.full_name ILIKE {{ '%' + searchInput.value + '%' }}
17 OR c.email ILIKE {{ '%' + searchInput.value + '%' }}
18 OR c.company_name ILIKE {{ '%' + searchInput.value + '%' }})
19 -- Additional status filter from a Select component:
20 AND ({{ statusFilter.value === '' || statusFilter.value === null }}
21 OR c.status = {{ statusFilter.value }})
22ORDER BY c.full_name ASC
23LIMIT 100;

Expected result: Searching 'Acme' returns all customers with 'Acme' in their name, email, or company. Empty search returns all records.

4

Build a client-side filter using a transformer

For datasets under ~500 rows that are already loaded, use a transformer to filter without hitting the database. Create a standalone transformer named filteredCustomers. It reads from getCustomers.data and applies filter logic using JavaScript. The Table's data source is bound to {{ filteredCustomers.value }} instead of {{ getCustomers.data }}. This is instant — no query latency — but only searches already-loaded data.

typescript
1// Transformer: filteredCustomers
2// Reads from getCustomers.data and searchInput.value
3// Important: transformers are READ-ONLY — cannot trigger queries or setValue()
4
5const query = searchInput.value?.toLowerCase().trim() || '';
6const data = getCustomers.data;
7
8if (!query) {
9 return data; // Return all records when search is empty
10}
11
12return data.filter(row => {
13 const name = (row.full_name || '').toLowerCase();
14 const email = (row.email || '').toLowerCase();
15 const company = (row.company_name || '').toLowerCase();
16 return name.includes(query) || email.includes(query) || company.includes(query);
17});
18
19// Table component data source:
20// Data: {{ filteredCustomers.value }}

Expected result: The table filters instantly as the user types, without any database queries. {{ filteredCustomers.value.length }} shows the match count.

5

Add a status filter dropdown alongside the search input

Combine text search with a Select dropdown for category or status filtering. Add a Select named statusFilter with options from a query or static values. The SQL query already includes the status filter clause (see Step 3). Add a Change event handler on statusFilter that also triggers getCustomers. Now users can type a search term AND filter by status simultaneously — the AND clause in the SQL handles both conditions.

typescript
1// statusFilter Select component settings:
2// Options (static):
3// Label: All Statuses, Value: ''
4// Label: Active, Value: 'active'
5// Label: Inactive, Value: 'inactive'
6// Label: Pending, Value: 'pending'
7// Default Value: '' (All Statuses pre-selected)
8
9// Event handler on statusFilter:
10// Event: Change → Trigger query → getCustomers
11
12// Combined result count Text component:
13{{ getCustomers.data.length }} results
14{{ statusFilter.value ? ` (${statusFilter.value})` : '' }}
15{{ searchInput.value ? ` matching "${searchInput.value}"` : '' }}

Expected result: Users can search by text and filter by status simultaneously. Changing either control re-runs getCustomers with both filters applied.

6

Add a clear search button and result count display

Add a Button component with an X icon to the right of the search input to clear the search. Create a JS Query named clearSearch that calls searchInput.setValue('') and statusFilter.setValue('') to reset both filters, then triggers getCustomers to reload all records. Display a result count Text component showing how many records match: {{ getCustomers.data.length }} results.

typescript
1// JS Query: clearSearch
2// Trigger: Clear button click
3
4await searchInput.setValue('');
5await statusFilter.setValue('');
6await getCustomers.trigger();
7
8// Result count Text component:
9{{ getCustomers.isFetching
10 ? 'Searching...'
11 : `${getCustomers.data.length} result${getCustomers.data.length !== 1 ? 's' : ''}` }}
12
13// Show clear button only when search is active:
14// Clear button Hidden: {{ !searchInput.value && !statusFilter.value }}

Expected result: Clicking the X button clears both filters and reloads all records. Result count updates dynamically.

Complete working example

SQL Query: getCustomers (full search + filter)
1-- getCustomers full-text search with status filter
2-- Run on page load: ON
3-- Triggers: searchInput onChange (300ms debounce), statusFilter onChange
4
5SELECT
6 c.id,
7 c.full_name,
8 c.email,
9 c.company_name,
10 c.phone,
11 c.status,
12 c.created_at,
13 COUNT(*) OVER() AS total_count
14FROM customers c
15WHERE
16 -- Text search across multiple fields (empty = show all)
17 (
18 {{ searchInput.value === '' || searchInput.value === null || searchInput.value === undefined }}
19 OR LOWER(c.full_name) LIKE LOWER({{ '%' + (searchInput.value || '') + '%' }})
20 OR LOWER(c.email) LIKE LOWER({{ '%' + (searchInput.value || '') + '%' }})
21 OR LOWER(c.company_name) LIKE LOWER({{ '%' + (searchInput.value || '') + '%' }})
22 OR CAST(c.id AS TEXT) = {{ searchInput.value || '' }}
23 )
24 -- Status filter (empty = show all)
25 AND (
26 {{ statusFilter.value === '' || statusFilter.value === null }}
27 OR c.status = {{ statusFilter.value }}
28 )
29ORDER BY
30 CASE WHEN LOWER(c.full_name) LIKE LOWER({{ (searchInput.value || '') + '%' }}) THEN 0 ELSE 1 END,
31 c.full_name ASC
32LIMIT 200;

Common mistakes

Why it's a problem: Not handling the empty search case in the SQL query — when searchInput.value is empty, the ILIKE clause matches nothing and returns zero results

How to avoid: Add a conditional: ({{ searchInput.value === '' }} OR column ILIKE {{ '%' + searchInput.value + '%' }}). This returns all records when the search box is empty.

Why it's a problem: Trying to trigger a query from inside a transformer — transformers are read-only and cannot trigger queries

How to avoid: Move the filter trigger logic to an event handler on the searchInput Change event. The transformer only computes the filtered view from already-loaded data.

Why it's a problem: Not debouncing the search input — triggers a database query on every character, causing excessive load and a flickering UI

How to avoid: In the Change event handler, enable Debounce with 300ms in the Advanced Options. This batches rapid keypresses into a single query trigger.

Why it's a problem: Building the ILIKE pattern in the SQL query by concatenating in JavaScript (e.g., '%' + searchInput.value + '%') without sanitizing — not a SQL injection risk in Retool's parameterized queries but causes issues if searchInput contains SQL wildcard characters

How to avoid: Retool parameterizes query values, so SQL injection is prevented. However, if users search for literal '%' or '_' characters and you want them treated as literals, escape them: searchInput.value.replace(/%/g, '\\%').replace(/_/g, '\\_').

Best practices

  • Always debounce Change event handlers on search inputs — 300ms prevents a database query on every single keypress
  • Use the built-in Table search bar for quick prototypes and simple use cases; switch to server-side ILIKE for production apps with large datasets
  • Show a result count ('42 results') so users know the search worked and can gauge how specific to make their query
  • Add a Clear button to reset all filters at once — users should never have to manually clear multiple filter fields
  • Use ILIKE (PostgreSQL) or LOWER() + LIKE (MySQL) for case-insensitive search — never do case-sensitive substring matching for user-facing search
  • Sort results so exact matches or prefix matches appear before substring matches using a CASE expression in ORDER BY
  • For very large datasets (millions of rows), add a full-text search index (PostgreSQL tsvector) rather than relying on ILIKE — ILIKE performs a full table scan

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I am building a Retool customer management app with a Table component showing customers. I need: (1) a Text Input named searchInput that triggers a SQL query with 300ms debounce, (2) a PostgreSQL query that searches across full_name, email, and company_name using ILIKE with an empty search showing all results, (3) a status Select filter that works together with the text search via AND clause, (4) a Clear button that resets both filters. Write the complete solution: SQL query, event handler configs, and the clearSearch JS Query.

Retool Prompt

I have a Retool Table component showing 200 customer records loaded from a query. I want instant client-side filtering without hitting the database again. How do I create a transformer that filters getCustomers.data based on a searchInput.value? What should I set as the Table's data source?

Frequently asked questions

Can I search across data from multiple tables or queries at once?

Yes, with a SQL JOIN query that combines the tables and applies ILIKE across all relevant columns. Alternatively, run parallel filtered queries and display results in separate tables with their own result counts. For a unified 'global search' across unrelated entities, use a SQL UNION to combine results from multiple tables into one query.

How do I implement fuzzy search (matching despite typos) in Retool?

PostgreSQL has the pg_trgm extension for trigram similarity matching, which handles typos. Enable it with CREATE EXTENSION pg_trgm and use similarity() or % operator in your WHERE clause. For lighter fuzzy matching without extensions, use client-side JavaScript in a transformer with a library like Fuse.js loaded via Retool's Preloaded JS setting.

How do I make search results highlight the matching text?

This requires custom HTML rendering. In the Table component's column settings, set the column type to 'Custom' and write a custom component that wraps matches in a highlight span. Alternatively, use a List component instead of a Table and use a Text component with inline HTML or a custom component to render highlighted results.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Learning is great. Shipping is faster with help.

Our engineers have built 600+ apps on Retool and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.