# How to Create Dropdown Menus in Retool

- Tool: Retool
- Difficulty: Beginner
- Time required: 15-20 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

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

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

## Before you start

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

```
// Static options example — status dropdown
// Label: Active    Value: active
// Label: Inactive  Value: inactive
// Label: Pending   Value: pending

// Access selected value elsewhere:
{{ statusSelect.value }}
// 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.

```
-- SQL query: getCategories (Run on page load: ON)
SELECT id, category_name
FROM categories
WHERE is_active = true
ORDER BY category_name ASC;

// Select component Inspector:
// Options: {{ getCategories.data }}
// Option Label: {{ item.category_name }}
// Option Value: {{ item.id }}
// 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.

```
// In a SQL query — filter by selected category:
SELECT * FROM products
WHERE category_id = {{ categorySelect.value }}
ORDER BY name ASC;

// In a Text component — display selection description:
{{ getCategories.data.find(c => c.id === categorySelect.value)?.description || '' }}

// In a Hidden property — show field only for a specific selection:
{{ 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.

```
// Inspector → Interaction → Event Handlers
// Event: Change
// Action: Trigger query → getProducts

// To also reset a child select:
// Second handler on the same Change event:
// Action: Control component → productSelect → resetValue

// To navigate on selection:
// 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.

```
// Multiselect value is always an array:
{{ multiselect1.value }}
// Returns: [1, 4, 7]

// SQL with PostgreSQL ANY():
SELECT * FROM products
WHERE category_id = ANY({{ multiselect1.value }});

// Display as comma-separated labels:
{{ multiselect1.value.map(v => getCategories.data.find(c => c.id === v)?.category_name).join(', ') }}

// Pass to REST API as query string:
// 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'].

```
// Transformer: categoryTree (reads from getCategories.data)
// Input: flat array with parent_id field
// Output: nested tree for Cascader

const categories = getCategories.data;
const rootItems = categories.filter(c => !c.parent_id);

function buildTree(parentId) {
  return categories
    .filter(c => c.parent_id === parentId)
    .map(c => ({
      label: c.name,
      value: c.id,
      children: buildTree(c.id),
    }));
}

return rootItems.map(c => ({
  label: c.name,
  value: c.id,
  children: buildTree(c.id),
}));

// Cascader selected value:
{{ cascader1.value }}
// 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.

```
// Default Value expressions:

// From selected table row:
{{ table1.selectedRow.data.category_id }}

// From URL param:
{{ url.searchParams.categoryId }}

// Static default:
// 'active' (type directly in the Default Value field)

// JS Query: runtime update (async!)
await categorySelect.setValue(newCategoryId);
// Do NOT read categorySelect.value on the next line —
// setValue is async; the value may not have updated yet.
```

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

## Complete code example

File: `SQL Query: getCategories with dependent getProducts`

```sql
-- Query 1: getCategories
-- Settings: Run on page load: ON
SELECT
  id,
  category_name,
  description
FROM categories
WHERE is_active = true
ORDER BY category_name ASC;

-- Query 2: getProducts
-- Settings: Run on page load: ON
-- Trigger: categorySelect onChange event
SELECT
  p.id,
  p.name,
  p.price,
  p.stock_qty,
  c.category_name
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE
  ({{ categorySelect.value !== null && categorySelect.value !== '' }}
   AND p.category_id = {{ categorySelect.value }})
  OR {{ categorySelect.value === null || categorySelect.value === '' }}
ORDER BY p.name ASC;
```

## Common mistakes

- **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** — undefined Fix: Always use stable database keys as Option Value. Labels can change without breaking query parameters.
- **Forgetting to add a Change event handler — the dependent query does not run when the user changes the selection, so related data stays stale** — undefined Fix: 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.
- **Using await select1.setValue(newValue) and reading select1.value on the very next line expecting the updated value** — undefined Fix: 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);
- **Configuring a Cascader by trying to pass a flat array — it renders empty or throws an error because it expects a nested tree structure** — undefined Fix: 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

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

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-create-dropdown-menus-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-create-dropdown-menus-in-retool
