Retool autocomplete is built on the Select and Multiselect components with the Filterable toggle enabled. For client-side filtering, set the options to {{ query1.data }} and Retool filters client-side as the user types. For server-side typeahead, connect a Text Input's Change event to a query that uses {{ textInput1.value }} in its WHERE clause, then bind the Select's options to that query's data.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 15-20 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 2026 |
Building Typeahead Autocomplete with Retool's Select Component
Autocomplete in Retool is an input-level typeahead — users start typing and see a filtered list of options without pressing Enter or clicking a search button. Retool implements this through the Select and Multiselect components with the Filterable property enabled.
There are two approaches: client-side filtering (load all options once, filter in the browser) and server-side filtering (trigger a new query for each keypress, returning only matching results). Client-side works well for lists under ~500 items. Server-side is required for large datasets like customer or product catalogs.
This tutorial covers enabling filtering on a Select, wiring dynamic query data as options, and building a debounced server-side typeahead with a Text Input. It is distinct from the search-functionality tutorial, which covers app-wide search across table data — autocomplete here refers specifically to the input-level typeahead user experience.
Prerequisites
- A Retool app with at least one resource (database or REST API) configured
- A dataset with 10+ records to demonstrate filtering
- Basic familiarity with adding components and the Inspector panel
Step-by-step guide
Add a Select component and enable Filterable
Drag a Select component from the component panel onto the canvas. In the Inspector's General section, find the Filterable toggle and enable it. When Filterable is on, a search box appears at the top of the Select dropdown when it opens. As the user types in the search box, options are filtered client-side using a case-insensitive substring match against option labels. This is the quickest path to autocomplete for static or pre-loaded option lists.
Expected result: The Select dropdown opens with a search input at the top. Typing filters the visible options in real time.
Load dynamic options from a query
Create a Resource Query named getCustomers with 'Run on page load' enabled. Set the Select component's Data Source to Custom in the Inspector, and set Options to {{ getCustomers.data }}. Set Option Label to {{ item.full_name }} and Option Value to {{ item.id }}. The Select now shows customer names as labels and passes customer IDs as values. When the user types in the search box, Retool filters by label (full_name) client-side.
1-- SQL query: getCustomers (Run on page load: ON)2SELECT id, full_name, email3FROM customers4ORDER BY full_name ASC5LIMIT 500;67// Select component Inspector settings:8// Options: {{ getCustomers.data }}9// Option Label: {{ item.full_name }}10// Option Value: {{ item.id }}Expected result: The dropdown populates with customer names. Typing filters the list instantly without hitting the database again.
Build a server-side typeahead with a Text Input
For large datasets, replace the Filterable Select approach with a dedicated Text Input for search input. Add a Text Input named customerSearchInput with Placeholder 'Type to search customers…'. Create a Resource Query named searchCustomers that uses the input value in a SQL ILIKE clause. Enable 'Run on page load' on searchCustomers so it loads an initial result set. Set a Select component's options to {{ searchCustomers.data }}.
1-- SQL query: searchCustomers2-- Run on page load: ON3-- Event: customerSearchInput onChange triggers this query4SELECT id, full_name, email5FROM customers6WHERE full_name ILIKE {{ '%' + customerSearchInput.value + '%' }}7 OR email ILIKE {{ '%' + customerSearchInput.value + '%' }}8ORDER BY full_name ASC9LIMIT 20;Expected result: Typing in customerSearchInput triggers searchCustomers and the Select updates with matching results.
Add a debounced Change event handler to the Text Input
Without debouncing, every keypress triggers a database query, which wastes resources and causes flickering. In customerSearchInput's Inspector → Interaction → Event Handlers, add a Change event that triggers searchCustomers. In the event handler's Advanced Options, enable 'Debounce' and set the debounce time to 300 milliseconds. This delays the query trigger until the user pauses typing for 300ms, reducing queries from one per character to roughly one per word.
Expected result: Typing 'Acme Corp' in the search input triggers only 1-2 queries instead of 8 (one per character).
Configure a Multiselect for multi-value autocomplete
Drag a Multiselect component onto the canvas for cases where users need to select multiple values (tags, categories, assignees). Enable Filterable in the Inspector. Set its Options, Option Label, and Option Value the same way as the Select. The selected values are accessible as {{ multiselect1.value }} which returns an array. Use {{ multiselect1.value.join(', ') }} to display selections as a comma-separated string, or pass the array directly to a SQL query using {{ multiselect1.value }}.
1// SQL query using multiselect values (PostgreSQL ANY):2SELECT * FROM orders3WHERE customer_id = ANY({{ multiselect1.value }});45// Or with IN clause (cast to text):6SELECT * FROM tags7WHERE name IN ({{ multiselect1.value.map(v => `'${v}'`).join(',') }});Expected result: Multiselect shows selected values as tags inside the input. {{ multiselect1.value }} returns an array like [1, 4, 7].
Display the selected value label elsewhere in the app
After the user selects an option, you often need to display the human-readable label (e.g., customer name) somewhere other than the select itself. The Select's .value property holds the option value (e.g., customer ID). To display the label, look it up in the options array: {{ getCustomers.data.find(c => c.id === customerSelect.value)?.full_name || '' }}. Alternatively, access .selectedOption which is available on Select components in newer Retool versions.
1// Display the selected customer's full name:2{{ getCustomers.data.find(c => c.id === customerSelect.value)?.full_name || 'No customer selected' }}34// Display selected customer's email:5{{ getCustomers.data.find(c => c.id === customerSelect.value)?.email || '' }}67// With selectedOption (Retool v3.x+):8{{ customerSelect.selectedOption?.label || '' }}Expected result: A Text component elsewhere shows the selected customer's full name reactively as the user changes selection.
Complete working example
1// This is a Resource Query (SQL), not a JS Query.2// Shown here for reference alongside the JS event handler.34// --- SQL Query: searchCustomers ---5// Run on page load: ON6// Parameters: none (reads customerSearchInput.value directly)7/*8SELECT9 id,10 full_name,11 email,12 company_name13FROM customers14WHERE15 ({{ customerSearchInput.value !== '' }} AND (16 full_name ILIKE {{ '%' + customerSearchInput.value + '%' }}17 OR email ILIKE {{ '%' + customerSearchInput.value + '%' }}18 OR company_name ILIKE {{ '%' + customerSearchInput.value + '%' }}19 ))20 OR {{ customerSearchInput.value === '' }}21ORDER BY full_name ASC22LIMIT 20;23*/2425// --- customerSearchInput event handler (Inspector → Interaction) ---26// Event: Change27// Action: Trigger query → searchCustomers28// Debounce: 300ms2930// --- customerSelect component settings ---31// Options: {{ searchCustomers.data }}32// Option Label: {{ item.full_name }}33// Option Value: {{ item.id }}34// Filterable: OFF (server handles filtering)35// Loading: {{ searchCustomers.isFetching }}Common mistakes
Why it's a problem: Forgetting to enable 'Run on page load' on the initial query — the Select renders empty on load with no options until the user types something
How to avoid: Enable 'Run on page load' on the search query so it loads an initial result set (all records or top 20) before the user types anything.
Why it's a problem: Leaving Filterable enabled on the Select when also using a server-side Text Input search — this creates a confusing double-search UX
How to avoid: When using a dedicated Text Input for server-side search, set Filterable to OFF on the Select component. The Text Input handles the search; the Select just displays results.
Why it's a problem: Not debouncing the Change event handler — triggers a database query on every single keypress, causing slowness and excessive load
How to avoid: In the Change event handler's Advanced Options, enable Debounce with 300ms. This batches rapid keypresses into a single query trigger.
Why it's a problem: Referencing {{ select1.selectedOption.label }} before Retool v3 — selectedOption was added in later versions
How to avoid: Use {{ options.find(o => o.value === select1.value)?.label }} or look up the label from the source query data array using Array.find().
Best practices
- Use client-side Filterable for lists under 500 items — no query overhead, instant filtering, simpler setup
- Use server-side typeahead for large datasets — always add a LIMIT clause (20-50 results) to avoid loading thousands of rows
- Set the Select's Loading property to {{ searchCustomers.isFetching }} to show a spinner while the query runs, improving perceived performance
- Debounce Text Input Change handlers at 300-500ms to avoid flooding your database with queries on every keypress
- Disable the Select's own Filterable toggle when using server-side search — otherwise users see two search boxes (one built-in, one your Text Input)
- For Multiselect values in SQL, use the ANY() operator with a parameterized array rather than building IN() strings manually to avoid SQL injection
- Provide a meaningful Placeholder on the Search Text Input that explains what to search for: 'Search by name or email…'
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to build a customer autocomplete field in Retool. I have a PostgreSQL database with a customers table (id, full_name, email, company_name). I want a typeahead where users type a name and see matching customers, then select one. The customer table has 50,000 rows, so I need server-side filtering. Show me: (1) the SQL query named searchCustomers using ILIKE and LIMIT 20, (2) how to configure a Text Input named customerSearchInput with a debounced Change event handler, (3) how to bind a Select component's options to searchCustomers.data with full_name as label and id as value.
In my Retool app, I have a Select component named customerSelect. How do I make it filterable so users can type to narrow down options? What is the Filterable toggle and where is it in the Inspector? Should I use the built-in Filterable option or a separate Text Input for search?
Frequently asked questions
Can I show additional details (like email) alongside the name in the autocomplete dropdown?
Yes. In the Option Label field, combine multiple fields using a template literal expression: {{ item.full_name + ' — ' + item.email }}. The label is what users see in the dropdown; the value is what gets stored when they select an option.
How do I pre-select a value in a filterable Select component?
Set the component's Default Value to the option value (not label) you want pre-selected, for example {{ table1.selectedRow.data.customer_id }}. The Select will show the corresponding label. If the options are loaded from a query, ensure the query completes before the Default Value is evaluated — enable 'Run on page load' on the query.
What is the difference between Retool's autocomplete and the code editor autocomplete?
This tutorial covers the Select component's Filterable typeahead for end-user forms. Retool also has autocomplete in its code editor (JS Query editor and Inspector expression fields) that suggests component names, query properties, and JavaScript APIs. These are separate features — the editor autocomplete helps you build apps; the Select autocomplete is for end users interacting with the deployed app.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation