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.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 15-20 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 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
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.
1// Static options example — status dropdown2// Label: Active Value: active3// Label: Inactive Value: inactive4// Label: Pending Value: pending56// 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 }}.
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.
1-- SQL query: getCategories (Run on page load: ON)2SELECT id, category_name3FROM categories4WHERE is_active = true5ORDER BY category_name ASC;67// 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.
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.
1// In a SQL query — filter by selected category:2SELECT * FROM products3WHERE category_id = {{ categorySelect.value }}4ORDER BY name ASC;56// In a Text component — display selection description:7{{ getCategories.data.find(c => c.id === categorySelect.value)?.description || '' }}89// 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.
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.
1// Inspector → Interaction → Event Handlers2// Event: Change3// Action: Trigger query → getProducts45// To also reset a child select:6// Second handler on the same Change event:7// Action: Control component → productSelect → resetValue89// 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.
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.
1// Multiselect value is always an array:2{{ multiselect1.value }}3// Returns: [1, 4, 7]45// SQL with PostgreSQL ANY():6SELECT * FROM products7WHERE category_id = ANY({{ multiselect1.value }});89// Display as comma-separated labels:10{{ multiselect1.value.map(v => getCategories.data.find(c => c.id === v)?.category_name).join(', ') }}1112// 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.
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'].
1// Transformer: categoryTree (reads from getCategories.data)2// Input: flat array with parent_id field3// Output: nested tree for Cascader45const categories = getCategories.data;6const rootItems = categories.filter(c => !c.parent_id);78function buildTree(parentId) {9 return categories10 .filter(c => c.parent_id === parentId)11 .map(c => ({12 label: c.name,13 value: c.id,14 children: buildTree(c.id),15 }));16}1718return rootItems.map(c => ({19 label: c.name,20 value: c.id,21 children: buildTree(c.id),22}));2324// Cascader selected value:25{{ cascader1.value }}26// Returns: ['electronics', 'phones'] for a 2-level selectionExpected result: The Cascader shows a two-level dropdown. Selecting Electronics then Phones sets cascader1.value to ['electronics', 'phones'].
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.
1// Default Value expressions:23// From selected table row:4{{ table1.selectedRow.data.category_id }}56// From URL param:7{{ url.searchParams.categoryId }}89// Static default:10// 'active' (type directly in the Default Value field)1112// 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
1-- Query 1: getCategories2-- Settings: Run on page load: ON3SELECT4 id,5 category_name,6 description7FROM categories8WHERE is_active = true9ORDER BY category_name ASC;1011-- Query 2: getProducts12-- Settings: Run on page load: ON13-- Trigger: categorySelect onChange event14SELECT15 p.id,16 p.name,17 p.price,18 p.stock_qty,19 c.category_name20FROM products p21JOIN categories c ON p.category_id = c.id22WHERE23 ({{ 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation