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

How to Create Dropdown Menus in Retool

Create dropdown menus in Retool using the Select component. Set options manually in the Inspector or dynamically from a query by setting Options to {{ query1.data }}, Option Label to {{ item.fieldName }}, and Option Value to {{ item.id }}. Access the selected value anywhere in the app with {{ select1.value }} and trigger actions by adding a Change event handler.

What you'll learn

  • Configure a Select component with static manual options and dynamic query-driven options
  • Map query data to Option Label and Option Value separately using {{ item.name }} syntax
  • Access the selected value with {{ select1.value }} and trigger actions via Change event handlers
  • Use Multiselect for multi-value selections returning an array via {{ multiselect1.value }}
  • Build a Cascader component for hierarchical nested option trees
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

Create dropdown menus in Retool using the Select component. Set options manually in the Inspector or dynamically from a query by setting Options to {{ query1.data }}, Option Label to {{ item.fieldName }}, and Option Value to {{ item.id }}. Access the selected value anywhere in the app with {{ select1.value }} and trigger actions by adding a Change event handler.

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

Configuring Select, Multiselect, and Cascader Dropdowns in Retool

Dropdown menus in Retool are implemented through three components: Select (single value), Multiselect (array of values), and Cascader (hierarchical selection). All three live in the component panel under the Input or Form categories. Each supports both static (manually entered) options and dynamic options loaded from queries.

This tutorial covers the complete lifecycle: adding the component, configuring options, mapping label and value fields, reading the selected value in other components, and triggering queries from the Change event. It also covers the Cascader component's nested tree option format.

The key distinction from the autocomplete tutorial: dropdowns are selection-from-a-list components. Autocomplete adds a Filterable typeahead layer on top. The patterns here apply to any dropdown regardless of whether search is enabled.

Prerequisites

  • A Retool app with at least one resource configured
  • Familiarity with adding components from the component panel and using the Inspector

Step-by-step guide

1

Add a Select component and set manual options

Drag a Select component from the Input section of the component panel. In the Inspector's General section, set the Data Source to 'Manual' (the default). Click '+ Add option' to add entries. Each option has a Label (displayed to the user) and a Value (what is stored and referenced in expressions). For example, Label: 'Active', Value: 'active'. Add as many options as needed. The component is automatically named select1 — rename it in the Name field at the top of the Inspector.

typescript
1// Static options example — status dropdown
2// Label: Active Value: active
3// Label: Inactive Value: inactive
4// Label: Pending Value: pending
5
6// Access selected value elsewhere:
7{{ statusSelect.value }}
8// Returns: 'active' | 'inactive' | 'pending'

Expected result: A dropdown with three options appears. Selecting one stores the option value accessible via {{ statusSelect.value }}.

2

Load dynamic options from a query

When options change frequently or exist in a database, load them dynamically. Create a Resource Query named getCategories with 'Run on page load' enabled. Select the Select component and change Data Source from 'Manual' to 'Mapped'. Set Options to {{ getCategories.data }}. Set Option Label to {{ item.category_name }} and Option Value to {{ item.id }}. Retool iterates over the array, using each row's category_name as the display label and id as the stored value.

typescript
1-- SQL query: getCategories (Run on page load: ON)
2SELECT id, category_name
3FROM categories
4WHERE is_active = true
5ORDER BY category_name ASC;
6
7// Select component Inspector:
8// Options: {{ getCategories.data }}
9// Option Label: {{ item.category_name }}
10// Option Value: {{ item.id }}
11// Placeholder: 'Select a category'

Expected result: The dropdown populates with category names from the database. Selecting one stores the corresponding category ID.

3

Reference the selected value in other components

Use {{ select1.value }} anywhere in the app to read the currently selected option's value. This is reactive — when the user changes the selection, all expressions referencing select1.value update automatically. Common uses: filter a Table's data source query, display related information in a Text component, or control another Select's options as a dependent dropdown.

typescript
1// In a SQL query — filter by selected category:
2SELECT * FROM products
3WHERE category_id = {{ categorySelect.value }}
4ORDER BY name ASC;
5
6// In a Text component — display selection description:
7{{ getCategories.data.find(c => c.id === categorySelect.value)?.description || '' }}
8
9// In a Hidden property — show field only for a specific selection:
10{{ categorySelect.value !== 'digital' }}

Expected result: Table and Text components update instantly when a new option is selected in the dropdown.

4

Add a Change event handler to trigger a query

When the dropdown selection changes, you often need to trigger a query to refresh related data. Select the Select component. In Inspector → Interaction → Event Handlers, click '+ Add'. Set Event to 'Change', Action to 'Trigger query', and choose the query to run (e.g., getProducts). Each time the user selects a different option, the query runs automatically with the new {{ categorySelect.value }}. Add multiple event handlers if you need to trigger multiple queries or clear other components.

typescript
1// Inspector → Interaction → Event Handlers
2// Event: Change
3// Action: Trigger query → getProducts
4
5// To also reset a child select:
6// Second handler on the same Change event:
7// Action: Control component → productSelect → resetValue
8
9// To navigate on selection:
10// Action: Open URL → /app/{{ categorySelect.value }}

Expected result: Changing the category selection immediately triggers getProducts and the product list refreshes.

5

Configure a Multiselect for multi-value selection

Drag a Multiselect component when users need to select several values (tags, roles, filter criteria). Configuration is identical to Select: set Options from a query, set Option Label and Option Value. The key difference: {{ multiselect1.value }} returns an array (e.g., [1, 4, 7]) instead of a single value. Use this array in SQL with the ANY() operator or pass it to a REST API as a comma-separated string.

typescript
1// Multiselect value is always an array:
2{{ multiselect1.value }}
3// Returns: [1, 4, 7]
4
5// SQL with PostgreSQL ANY():
6SELECT * FROM products
7WHERE category_id = ANY({{ multiselect1.value }});
8
9// Display as comma-separated labels:
10{{ multiselect1.value.map(v => getCategories.data.find(c => c.id === v)?.category_name).join(', ') }}
11
12// Pass to REST API as query string:
13// URL parameter: ids={{ multiselect1.value.join(',') }}

Expected result: Users can select multiple options. {{ multiselect1.value }} returns an array with all selected values.

6

Build a Cascader for hierarchical options (Category > Subcategory)

The Cascader component handles nested hierarchical selections like Country > State > City or Category > Subcategory. Its Options expect a tree structure with children arrays. Build the tree in a transformer. Set the Cascader's Options to {{ categoryTree.value }} where categoryTree is a transformer that restructures flat query data into the tree format. The selected value is {{ cascader1.value }} which returns an array of selected values at each level, e.g., ['electronics', 'phones'].

typescript
1// Transformer: categoryTree (reads from getCategories.data)
2// Input: flat array with parent_id field
3// Output: nested tree for Cascader
4
5const categories = getCategories.data;
6const rootItems = categories.filter(c => !c.parent_id);
7
8function buildTree(parentId) {
9 return categories
10 .filter(c => c.parent_id === parentId)
11 .map(c => ({
12 label: c.name,
13 value: c.id,
14 children: buildTree(c.id),
15 }));
16}
17
18return rootItems.map(c => ({
19 label: c.name,
20 value: c.id,
21 children: buildTree(c.id),
22}));
23
24// Cascader selected value:
25{{ cascader1.value }}
26// Returns: ['electronics', 'phones'] for a 2-level selection

Expected result: The Cascader shows a two-level dropdown. Selecting Electronics then Phones sets cascader1.value to ['electronics', 'phones'].

7

Set a default selected value programmatically

To pre-select a dropdown option, use the Default Value field in the Inspector. Set it to the value (not the label) of the option to pre-select. For dynamic defaults from a table row: {{ table1.selectedRow.data.category_id }}. For defaults from URL params: {{ url.searchParams.category }}. Remember: Default Value is applied at component mount time. To change the selection after mount, use await select1.setValue(newValue) from a JS Query — and remember setValue() is asynchronous.

typescript
1// Default Value expressions:
2
3// From selected table row:
4{{ table1.selectedRow.data.category_id }}
5
6// From URL param:
7{{ url.searchParams.categoryId }}
8
9// Static default:
10// 'active' (type directly in the Default Value field)
11
12// JS Query: runtime update (async!)
13await categorySelect.setValue(newCategoryId);
14// Do NOT read categorySelect.value on the next line —
15// setValue is async; the value may not have updated yet.

Expected result: The dropdown renders with the pre-selected option highlighted.

Complete working example

SQL Query: getCategories with dependent getProducts
1-- Query 1: getCategories
2-- Settings: Run on page load: ON
3SELECT
4 id,
5 category_name,
6 description
7FROM categories
8WHERE is_active = true
9ORDER BY category_name ASC;
10
11-- Query 2: getProducts
12-- Settings: Run on page load: ON
13-- Trigger: categorySelect onChange event
14SELECT
15 p.id,
16 p.name,
17 p.price,
18 p.stock_qty,
19 c.category_name
20FROM products p
21JOIN categories c ON p.category_id = c.id
22WHERE
23 ({{ categorySelect.value !== null && categorySelect.value !== '' }}
24 AND p.category_id = {{ categorySelect.value }})
25 OR {{ categorySelect.value === null || categorySelect.value === '' }}
26ORDER BY p.name ASC;

Common mistakes when creating Dropdown Menus in Retool

Why it's a problem: Setting Option Value to {{ item.category_name }} (a string label) instead of {{ item.id }} (an integer key) — this causes joined queries to fail when category names change

How to avoid: Always use stable database keys as Option Value. Labels can change without breaking query parameters.

Why it's a problem: Forgetting to add a Change event handler — the dependent query does not run when the user changes the selection, so related data stays stale

How to avoid: In Inspector → Interaction → Event Handlers, add a Change event that triggers the dependent query. For multiple dependent queries, add multiple event handlers on the same Change event.

Why it's a problem: Using await select1.setValue(newValue) and reading select1.value on the very next line expecting the updated value

How to avoid: setValue() is asynchronous — the component re-renders after the await, but the read happens before the re-render cycle completes. Read the new value from the variable you just passed to setValue() instead: const newVal = computedValue; await select1.setValue(newVal); console.log(newVal);

Why it's a problem: Configuring a Cascader by trying to pass a flat array — it renders empty or throws an error because it expects a nested tree structure

How to avoid: Use a transformer to convert the flat array (with parent_id fields) into the nested children tree format before passing it to the Cascader's Options property.

Best practices

  • Always name Select components descriptively (categorySelect, statusSelect) rather than leaving them as select1 — this makes {{ }} expressions in other components self-documenting
  • Use Option Value to store the database ID or enum key, not the display label — display labels can change but IDs stay stable
  • Add ORDER BY to all queries that feed dropdown options to ensure consistent presentation
  • Set a meaningful Placeholder text like 'Select a category' — an empty dropdown with no placeholder is confusing to users
  • For dependent dropdowns (parent changes child options), always reset the child's value when the parent changes using resetValue() in the Change event handler
  • Use Multiselect when the use case allows 'none selected' as a valid state — a single Select with a placeholder for 'All' is harder to unset
  • Keep static option lists inside the component (manual options) to 3-10 items; anything larger should come from a query for maintainability

Still stuck?

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

ChatGPT Prompt

I am building a Retool app with two dropdown menus: a categorySelect (Select component) that loads categories from a SQL query named getCategories, and a productSelect that should reload when the category changes. The categories table has id and category_name columns. Show me: (1) the SQL for getCategories, (2) the Inspector settings for categorySelect (Options, Option Label, Option Value), (3) how to configure a Change event handler on categorySelect to trigger getProducts and reset productSelect, (4) the SQL for getProducts filtered by categorySelect.value.

Retool Prompt

I have a Retool Select component named statusSelect with static options: Active (value: active), Inactive (value: inactive), Archived (value: archived). I want it to default to 'active' on load, and when the user changes it I want it to trigger a query called refreshTable. How do I configure the Default Value and the Change event handler in the Inspector?

Frequently asked questions

How do I make a dropdown option disabled or greyed out without removing it?

Retool's Select component supports a disabled property per option in Mapped mode. In the Option configuration, add a Disabled expression: {{ item.is_inactive === true }}. The option appears in the list but cannot be selected and is visually greyed out.

Can I group dropdown options by category using an optgroup?

Yes, in newer versions of Retool's Select component. In Mapped options mode, set Option Group to a field from your query data (e.g., {{ item.category }}). Options sharing the same group value are visually grouped under a header in the dropdown. This is a display-only grouping and does not affect the stored value.

How do I clear a Select component value programmatically?

Call await select1.resetValue() from a JS Query to clear the selection back to the placeholder state, or await select1.setValue(null) to explicitly set it to null. Both work; resetValue() is more expressive about intent. Remember both are async — add await if you need the cleared state before proceeding.

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.