# How to Use Conditional Logic in Retool

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

## TL;DR

Retool conditional logic works in three places: {{ }} expressions using ternary operators for dynamic values, the Hidden property for component visibility, and 'Only run when' conditions on event handlers to gate query execution. For complex branching, use JS Queries with standard if/else or switch statements and reference component values directly.

## Branching and Conditional Behavior in Retool

Conditional logic in Retool controls what users see and what actions execute based on data and user interactions. There are three main places where conditionals live: inside {{ }} expression bindings on any component property, in the Hidden property to show or hide components, and in the 'Only run when' field on event handlers.

Retool's {{ }} expressions support any valid JavaScript expression, including ternary operators, logical AND/OR short-circuit evaluation, and nullish coalescing. For more complex branching that involves multiple conditions and side effects (like triggering different queries based on a status value), use a JS Query with standard if/else or switch statements.

This tutorial covers all three approaches with practical examples: changing button labels dynamically, showing error banners conditionally, gating destructive actions with confirmation checks, and routing workflow logic based on form values.

## Before you start

- A Retool account (Cloud or Self-hosted)
- Familiarity with JavaScript ternary operators and basic boolean logic
- Understanding of Retool state variables and component properties
- A Retool app with at least a few components to experiment with

## Step-by-step guide

### 1. Write ternary expressions in {{ }} bindings

Any component property that accepts a {{ }} expression can contain a JavaScript ternary operator. The syntax is {{ condition ? valueIfTrue : valueIfFalse }}. This is useful for dynamic labels, colors, icons, and text. You can reference any component value, query result, or state variable in the condition. Expressions can be nested, but keep them readable — move complex logic to a transformer or JS Query if the expression exceeds one line.

```
// Button label that changes based on form state
{{ isEditing.value ? 'Save changes' : 'Edit' }}

// Text color based on order status
{{ order.status === 'overdue' ? '#e53e3e' : '#38a169' }}

// Dynamic greeting based on query data
{{ currentUser.data?.name ? `Welcome back, ${currentUser.data.name}` : 'Welcome' }}

// Nullish coalescing for safe default
{{ table1.selectedRow.data?.price ?? 0 }}
```

**Expected result:** The component property dynamically reflects the evaluated expression, updating automatically when its dependencies change.

### 2. Control component visibility with the Hidden property

Every Retool component has a Hidden property in the Inspector panel under the Layout section. Set it to a {{ }} expression that evaluates to true when the component should be hidden and false when it should be visible. This is the primary way to show/hide form fields, error banners, and action buttons based on conditions. Note: Hidden removes the component from the visual layout, but the component still exists in the app — its value is still accessible.

```
// Hide an error banner until there is an error
// (errorMessage state variable)
{{ !errorMessage.value }}

// Show a delete button only for admin users
{{ !current_user.groups.includes('admin') }}

// Show a 'No results' message only when query returns empty
{{ getData.data?.length > 0 }}

// Hide a field when a checkbox is unchecked
{{ !includeShipping.value }}
```

**Expected result:** Components appear and disappear based on the evaluated condition, reacting in real time to state and query changes.

### 3. Add 'Only run when' conditions to event handlers

In the Inspector panel under the Interaction section, event handlers (like onClick) have an 'Only run when' field. This is a {{ }} expression that must evaluate to true for the handler to fire. Use this to gate destructive operations (e.g., only delete if a row is selected), require confirmations, or prevent double-submissions. This is different from disabling the button — the button still appears and is clickable, but the handler will not run if the condition is false.

```
// Only run delete if a row is selected
{{ table1.selectedRow.data !== undefined }}

// Only run submit if the form is valid
{{ !form1.invalid }}

// Only run if the user confirmed (using a boolean state variable)
{{ hasConfirmed.value === true }}

// Only run if the selected status is 'pending'
{{ table1.selectedRow.data?.status === 'pending' }}
```

**Expected result:** The event handler's action (query trigger, navigation, etc.) only executes when the 'Only run when' expression evaluates to true.

### 4. Write branching logic in a JS Query

When your conditional logic involves multiple branches, side effects, or needs to trigger different queries based on conditions, use a JS Query with standard JavaScript if/else or switch statements. Reference Retool component values and state variables directly by name — no {{ }} wrapper needed inside JS Queries. Use early returns to handle edge cases before the main logic runs.

```
// JS Query: handleFormSubmit
const action = actionSelect.value; // 'create' | 'update' | 'delete'
const selectedId = table1.selectedRow.data?.id;

// Early return for validation
if (!textInput1.value || textInput1.value.trim() === '') {
  utils.showNotification({
    title: 'Name is required',
    notificationType: 'warning'
  });
  return;
}

// Branch based on action
switch (action) {
  case 'create':
    await createRecord.trigger();
    break;
  case 'update':
    if (!selectedId) {
      utils.showNotification({ title: 'Select a row to update', notificationType: 'error' });
      return;
    }
    await updateRecord.trigger();
    break;
  case 'delete':
    if (!selectedId) return;
    await modal1.open(); // Show confirmation modal
    break;
  default:
    console.log('Unknown action:', action);
}
```

**Expected result:** The JS Query executes the correct branch based on the runtime values of components and state variables.

### 5. Use logical operators for compound conditions

Combine multiple conditions using JavaScript's && (AND), || (OR), and ! (NOT) operators inside {{ }} expressions or JS Queries. Use short-circuit evaluation for safe property access. Logical AND (&&) is particularly useful for rendering patterns where you want a component to show only when multiple conditions are true.

```
// Show edit form only when a row is selected AND user is an admin
{{ table1.selectedRow.data !== undefined && current_user.groups.includes('admin') }}

// Disable a button if loading OR no row selected
{{ getData.isFetching || !table1.selectedRow.data }}

// Show success banner only when query succeeded and has data
{{ submitForm.data?.success === true && !submitForm.isFetching }}

// Safe nested access with logical AND
{{ userData.data && userData.data.profile && userData.data.profile.avatar }}
```

**Expected result:** Compound conditions correctly evaluate multiple factors before rendering a component or allowing an action.

### 6. Build a dynamic form with conditional field visibility

A practical pattern is showing or hiding form fields based on other field values. For example, showing a shipping address section only when 'Ship to different address' is checked, or showing a custom reason text field only when 'Other' is selected in a dropdown. Use state variables or component values directly in Hidden properties.

```
// Select component: reasonSelect with options
// 'Bug', 'Feature request', 'Question', 'Other'

// Text input: customReasonInput
// Hidden property:
{{ reasonSelect.value !== 'Other' }}

// Checkbox: billingSameAsShipping
// Shipping address Container Hidden property:
{{ billingSameAsShipping.value }}

// Number input: discountPercent
// Hidden property: only show for Managers
{{ !current_user.groups.includes('managers') }}
```

**Expected result:** Form fields appear and disappear dynamically based on other field values, creating a responsive multi-path form.

## Complete code example

File: `JS Query: conditionalWorkflowHandler`

```javascript
// Comprehensive conditional workflow handler
// Triggered by a 'Process Order' button click

const order = table1.selectedRow.data;
const userGroups = current_user.groups;

// === Guard clauses (early returns) ===
if (!order) {
  utils.showNotification({
    title: 'No order selected',
    description: 'Please select an order from the table first.',
    notificationType: 'warning'
  });
  return;
}

if (!userGroups.includes('ops') && !userGroups.includes('admin')) {
  utils.showNotification({
    title: 'Permission denied',
    description: 'Only Ops and Admin users can process orders.',
    notificationType: 'error'
  });
  return;
}

// === Branch by order status ===
switch (order.status) {
  case 'pending':
    await processOrder.trigger({ additionalScope: { orderId: order.id } });
    utils.showNotification({ title: 'Order processed', notificationType: 'success' });
    break;

  case 'processing':
    // Show confirmation modal before cancellation
    await isConfirmCancelOpen.setValue(true);
    break;

  case 'completed':
    utils.showNotification({
      title: 'Already completed',
      description: `Order #${order.id} is already complete.`,
      notificationType: 'info'
    });
    break;

  default:
    utils.showNotification({
      title: 'Unknown status',
      description: `Cannot process order with status: ${order.status}`,
      notificationType: 'error'
    });
}

// Refresh the orders table
await getOrders.trigger();
```

## Common mistakes

- **Using {{ }} syntax inside a JS Query instead of referencing component values directly** — undefined Fix: Inside JS Queries, reference components as textInput1.value, not as {{ textInput1.value }}. The {{ }} wrapper is only for component property bindings.
- **Accessing query1.data.property without optional chaining and getting 'Cannot read property' errors** — undefined Fix: Use optional chaining: {{ query1.data?.property }}. Query data is null before the query runs, so direct property access will throw.
- **Putting multi-branch logic with side effects in a transformer** — undefined Fix: Transformers are read-only and cannot trigger queries or set state. Move branching logic that triggers other queries or calls setValue() into a JS Query.
- **Using 'Only run when' without user feedback when the condition is false** — undefined Fix: Add a second event handler with the negated 'Only run when' condition that calls utils.showNotification() explaining why the action could not run.

## Best practices

- Prefer {{ condition ? a : b }} ternary expressions for simple two-outcome bindings and JS Query if/else for multi-branch logic.
- Use optional chaining (?.) in all {{ }} expressions that access query data — query1.data?.property prevents errors during loading states.
- Set the Hidden property (not Disabled) when a component should not be visible — Disabled still shows the component grayed out, which can confuse users.
- Use 'Only run when' on destructive actions (delete, archive) as an extra safety gate, separate from button disabling.
- Keep {{ }} expressions under one line. If your conditional is longer than ~80 characters, move it to a transformer or JS Query and reference the result.
- Always handle the falsy/loading case in conditionals — query1.data is null before a query runs, so {{ query1.data.length > 0 }} will throw without optional chaining.
- Avoid deeply nested ternaries in {{ }} bindings — they are hard to read and debug. Use a JS transformer to compute a value and reference it instead.

## Frequently asked questions

### What is the difference between Hidden and Disabled in Retool?

Hidden removes the component from the layout entirely — users cannot see it. Disabled keeps the component visible but grayed out and non-interactive. Use Hidden when the component is not relevant to the current state, and Disabled when you want to signal that an action is temporarily unavailable.

### Can I use if/else inside a {{ }} binding in Retool?

No. The {{ }} binding only accepts JavaScript expressions, not statements. Use a ternary operator (condition ? a : b) or logical operators (&& and ||) inside {{ }}. If you need if/else, move the logic to a JS Query or transformer and reference the output.

### How do I run different queries based on a dropdown selection in Retool?

Create a JS Query triggered by the dropdown's Change event handler. Inside the query, use a switch or if/else on select1.value to call the appropriate query with await queryName.trigger(). This gives you full programmatic control over which query runs based on the selected value.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-use-conditional-logic-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-use-conditional-logic-in-retool
