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

How to Create Dynamic Forms in Retool

Dynamic forms in Retool use the Hidden property on each component with a {{ }} boolean expression referencing other component values. Set a field's Hidden to {{ select1.value !== 'US' }} to show it only when select1 equals 'US'. Combine with dependent query triggers on Change event handlers to load child options on demand.

What you'll learn

  • Use the Hidden property with {{ }} expressions to conditionally show/hide form fields
  • Build dependent dropdowns where Country drives State options via query filtering
  • Reference sibling component values like {{ select1.value }} inside Hidden conditions
  • Use a Temporary State variable to track dynamic form state across field changes
  • Trigger re-queries automatically when upstream fields change using event handlers
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate8 min read20-30 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
TL;DR

Dynamic forms in Retool use the Hidden property on each component with a {{ }} boolean expression referencing other component values. Set a field's Hidden to {{ select1.value !== 'US' }} to show it only when select1 equals 'US'. Combine with dependent query triggers on Change event handlers to load child options on demand.

Quick facts about this guide
FactValue
ToolRetool
DifficultyIntermediate
Time required20-30 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 2026

Building Conditional Field Logic in Retool Forms

Dynamic forms change their visible fields based on what a user has already entered. In Retool, this is accomplished through the Hidden property available on every component. Instead of hiding an entire container, you write a short {{ }} expression referencing another component's value — when the expression evaluates to true, the field disappears; when false, it appears.

This tutorial focuses on the conditional-fields pattern: showing and hiding inputs on a single page based on upstream selections. You will build a form where a 'Customer Type' select drives whether business-specific fields appear, and where a Country dropdown loads state/region options from a filtered query. All interactions stay on one page — for multi-page wizard flows, see the multi-step-forms tutorial.

You will work with the Form component's built-in layout, Select components, Text Input components, the Hidden property in the Inspector's Layout section, and event handler chaining to re-run queries when parent field values change.

Prerequisites

  • A Retool app with at least one resource (database or REST API) configured
  • Familiarity with adding components from the component panel
  • Basic understanding of {{ }} expression syntax in the Inspector
  • A data source that can return country and state/region lists (or use static JSON)

Step-by-step guide

1

Add a Form component and set its layout

Drag a Form component from the component panel onto the canvas. In the Inspector's General section, set the Form's Title to 'Customer Registration'. The Form component acts as a logical grouping — it exposes form1.data (all child input values as an object) and form1.invalid (boolean) which you will use later for the submit button. Resize the form to span the full working width.

Expected result: A Form component named form1 appears on the canvas with a default Submit button.

2

Add a Customer Type Select and configure static options

Drag a Select component inside the Form. Rename it to customerTypeSelect in the Inspector. In General → Options, choose Manual and add two options: label 'Individual', value 'individual' and label 'Business', value 'business'. Set Placeholder to 'Select customer type'. This select will be the gating condition for all business-specific fields below.

Expected result: A dropdown showing 'Individual' and 'Business' options appears inside the form.

3

Add business-only fields and set their Hidden property

Add a Text Input named companyNameInput with label 'Company Name', and a Number Input named vatNumberInput with label 'VAT Number'. For each component, scroll to the Inspector's Layout section and find the Hidden toggle. Click the {{ fx }} icon to switch to expression mode. Enter: {{ customerTypeSelect.value !== 'business' }}. This hides the field whenever the customer type is not 'business'. Repeat for both business-specific inputs.

typescript
1// Hidden property expression for business-only fields
2{{ customerTypeSelect.value !== 'business' }}
3
4// For a field that shows only for individuals:
5{{ customerTypeSelect.value !== 'individual' }}

Expected result: Company Name and VAT Number fields disappear when 'Individual' is selected and reappear when 'Business' is selected.

4

Create a Country dropdown with dynamic options from a query

Add a Select component named countrySelect with label 'Country'. Create a new Resource Query named getCountries that returns a list of country records (id, name). Set the Select's Options Source to '{{ getCountries.data }}', Option Label to '{{ item.name }}', and Option Value to '{{ item.id }}'. Run getCountries on page load by enabling 'Run on page load' in the query's Settings tab.

typescript
1-- SQL query: getCountries
2SELECT id, name FROM countries ORDER BY name ASC;

Expected result: The Country dropdown populates with country names from your database on page load.

5

Create a dependent State/Region query and wire it to Country changes

Create a second Resource Query named getStates. Add a parameter to filter by the selected country: {{ countrySelect.value }}. In the query SQL, use that parameter. Back on countrySelect, go to Inspector → Interaction → Event Handlers. Add a handler: Event = 'Change', Action = 'Trigger query', Query = 'getStates'. Also add a second Change handler that calls stateSelect.resetValue() so stale selections clear when the country changes.

typescript
1-- SQL query: getStates
2SELECT id, name FROM states
3WHERE country_id = {{ countrySelect.value }}
4ORDER BY name ASC;

Expected result: When a country is selected, getStates runs and populates the State dropdown with the correct regions.

6

Add a State Select with a Hidden guard for countries without states

Add a Select component named stateSelect with label 'State / Region'. Set its Options to {{ getStates.data }}, label to {{ item.name }}, value to {{ item.id }}. In Layout → Hidden, enter: {{ getStates.data.length === 0 }}. This hides the state field entirely when the selected country has no state records, avoiding a confusing empty dropdown.

typescript
1// Hidden expression on stateSelect
2{{ getStates.data.length === 0 }}
3
4// Options configuration
5// Options source: {{ getStates.data }}
6// Option label: {{ item.name }}
7// Option value: {{ item.id }}

Expected result: State dropdown appears only for countries that have state records, disappears for others.

7

Wire the Submit button to read form1.data

Add a JS Query named submitForm. In the query body, read the consolidated form data from form1.data and call your save API or mutation. In the Form component's Inspector, set the Submit button's event handler to trigger submitForm. Add a 'Only run when' condition to the submitForm query: {{ !form1.invalid }} so it refuses to execute if required fields are empty.

typescript
1// JS Query: submitForm
2const payload = {
3 customerType: form1.data.customerTypeSelect,
4 country: form1.data.countrySelect,
5 state: form1.data.stateSelect,
6 // business fields will be undefined for individuals — filter them
7 ...(form1.data.customerTypeSelect === 'business' && {
8 companyName: form1.data.companyNameInput,
9 vatNumber: form1.data.vatNumberInput,
10 }),
11};
12
13return await saveCustomer.trigger({ additionalScope: { payload } });

Expected result: Clicking Submit runs submitForm only when the form passes validation, passing only relevant fields.

8

Test all conditional paths and verify Hidden logic

Preview the app (Ctrl+Shift+P or click Preview). Test each path: select Individual — business fields should vanish; select Business — they should appear. Pick a country with states — state dropdown appears; pick one without — it hides. Open the State Inspector in the Debug Panel (bottom of editor) and verify component values update correctly. Check form1.data in the State Inspector to confirm the payload structure matches expectations.

Expected result: All conditional paths work correctly; form1.data contains the right set of fields based on the active selection.

Complete working example

JS Query: submitForm
1// submitForm — reads form1.data, filters to relevant fields, submits
2// Set 'Only run when': {{ !form1.invalid }}
3
4const data = form1.data;
5const isBusinessCustomer = data.customerTypeSelect === 'business';
6
7// Build payload — only include business fields when applicable
8const payload = {
9 customer_type: data.customerTypeSelect,
10 country_id: data.countrySelect,
11 state_id: data.stateSelect || null,
12 email: data.emailInput,
13};
14
15if (isBusinessCustomer) {
16 payload.company_name = data.companyNameInput;
17 payload.vat_number = data.vatNumberInput;
18}
19
20try {
21 await insertCustomer.trigger({
22 additionalScope: { payload }
23 });
24 utils.showNotification({
25 title: 'Success',
26 description: 'Customer saved successfully.',
27 notificationType: 'success',
28 });
29 form1.reset();
30} catch (err) {
31 utils.showNotification({
32 title: 'Error',
33 description: err.message,
34 notificationType: 'error',
35 });
36 throw err;
37}

Common mistakes when creating Dynamic Forms in Retool

Why it's a problem: Inverted Hidden logic — writing {{ customerTypeSelect.value === 'business' }} instead of {{ customerTypeSelect.value !== 'business' }} causes the field to show for the wrong selection

How to avoid: Read Hidden as 'hide when this is true'. A field that should appear for business should have Hidden: {{ customerTypeSelect.value !== 'business' }}

Why it's a problem: Forgetting to reset child selects when the parent changes — stateSelect retains its previous value after changing the country, causing form1.data to contain a stale state ID

How to avoid: Add a second event handler on countrySelect Change that calls stateSelect.resetValue() before or after triggering getStates

Why it's a problem: Reading form1.data inside a transformer instead of a JS Query, then trying to call a mutation from it

How to avoid: Transformers are read-only and cannot trigger queries or set state. Move the submission logic to a JS Query and call the mutation with await saveQuery.trigger()

Why it's a problem: Using a static options array on dependent dropdowns instead of a filtered query, resulting in all states always visible

How to avoid: Create a separate getStates query with {{ countrySelect.value }} as a parameter, and trigger it from countrySelect's Change event handler

Best practices

  • Use the Form component wrapper so form1.data aggregates all child inputs into one object — avoids manually reading each component value
  • Always set Hidden expressions to evaluate truthy for hidden, not for visible — Hidden: true = the component is invisible
  • Reset dependent selects via resetValue() in the parent's Change event handler to prevent stale selections surviving country changes
  • Guard the submit query with 'Only run when: {{ !form1.invalid }}' instead of managing disabled state manually on the button
  • Keep conditional fields in dedicated containers (Container or Form Section components) if many fields share the same condition — one Hidden expression on the container is cleaner than individual ones
  • Use the State Inspector panel during development to watch component values update in real time as you test conditional paths
  • Avoid nesting Hidden expressions more than two levels deep — compound conditions become hard to debug; use a Temporary State variable to encode complex state instead

Still stuck?

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

ChatGPT Prompt

I am building a dynamic form in Retool. I have a Select component named customerTypeSelect with values 'individual' and 'business'. I want company name and VAT number Text Inputs to be hidden unless the user selects 'business'. I also have a countrySelect that should trigger a getStates query and populate a stateSelect, hiding the state dropdown when no states are returned. Write the Hidden property expressions for the business fields, the event handler configuration for countrySelect, and a JS Query called submitForm that reads form1.data and builds a payload that only includes business fields when the customer type is business.

Retool Prompt

In my Retool app I need conditional fields. For the companyNameInput component, what should I put in the Hidden property to hide it when customerTypeSelect.value is not 'business'? Also show me how to add an event handler to countrySelect that triggers getStates and resets stateSelect when the country changes.

Frequently asked questions

Can I show or hide an entire section of fields at once instead of field by field?

Yes. Wrap related fields in a Container component and set the Hidden property on the Container. All child components inherit the visibility, so you only need one Hidden expression rather than one per field.

Does hiding a field remove its value from form1.data?

No — hidden components still contribute their current value to form1.data. If you need to exclude a hidden field's value from the submission payload, filter it out explicitly in your JS Query based on the gating condition, as shown in the submitForm code example above.

How do I make a field required only when it is visible?

Set the component's Required property to an expression that mirrors the visibility condition: {{ customerTypeSelect.value === 'business' }}. This way, the field is only required when it is shown. form1.invalid will still incorporate this conditional requirement correctly.

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.