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

How to Use Retool's Autocomplete Feature

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.

What you'll learn

  • Enable search/filtering on a Select component by toggling 'Filterable' in the Inspector
  • Configure dynamic options from query data using {{ query1.data }} with item.label and item.value
  • Build a debounced typeahead that triggers a filtered API query from a Text Input's Change event
  • Reference the search text inside a query with {{ textInput1.value }} for server-side filtering
  • Use a Multiselect component for multi-value autocomplete with tag display
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read15-20 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
TL;DR

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.

Quick facts about this guide
FactValue
ToolRetool
DifficultyBeginner
Time required15-20 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 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

1

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.

2

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.

typescript
1-- SQL query: getCustomers (Run on page load: ON)
2SELECT id, full_name, email
3FROM customers
4ORDER BY full_name ASC
5LIMIT 500;
6
7// 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.

3

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 }}.

typescript
1-- SQL query: searchCustomers
2-- Run on page load: ON
3-- Event: customerSearchInput onChange triggers this query
4SELECT id, full_name, email
5FROM customers
6WHERE full_name ILIKE {{ '%' + customerSearchInput.value + '%' }}
7 OR email ILIKE {{ '%' + customerSearchInput.value + '%' }}
8ORDER BY full_name ASC
9LIMIT 20;

Expected result: Typing in customerSearchInput triggers searchCustomers and the Select updates with matching results.

4

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).

5

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 }}.

typescript
1// SQL query using multiselect values (PostgreSQL ANY):
2SELECT * FROM orders
3WHERE customer_id = ANY({{ multiselect1.value }});
4
5// Or with IN clause (cast to text):
6SELECT * FROM tags
7WHERE 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].

6

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.

typescript
1// Display the selected customer's full name:
2{{ getCustomers.data.find(c => c.id === customerSelect.value)?.full_name || 'No customer selected' }}
3
4// Display selected customer's email:
5{{ getCustomers.data.find(c => c.id === customerSelect.value)?.email || '' }}
6
7// 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

JS Query: searchCustomers (server-side typeahead)
1// This is a Resource Query (SQL), not a JS Query.
2// Shown here for reference alongside the JS event handler.
3
4// --- SQL Query: searchCustomers ---
5// Run on page load: ON
6// Parameters: none (reads customerSearchInput.value directly)
7/*
8SELECT
9 id,
10 full_name,
11 email,
12 company_name
13FROM customers
14WHERE
15 ({{ 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 ASC
22LIMIT 20;
23*/
24
25// --- customerSearchInput event handler (Inspector → Interaction) ---
26// Event: Change
27// Action: Trigger query → searchCustomers
28// Debounce: 300ms
29
30// --- 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.

ChatGPT Prompt

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.

Retool Prompt

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.

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.