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

How to Create Modal Dialogs in Retool

Create modals in Retool by adding a Modal component from the component panel. Open it with modal1.open() from a button click event handler or JS Query. Pass data into the modal by referencing table1.selectedRow.data inside modal components. Close it with modal1.close(). For confirmation dialogs, add Confirm and Cancel buttons that trigger the action query or close the modal.

What you'll learn

  • Add a Modal or Drawer component and open it programmatically with modal1.open()
  • Pass context from a table row selection into a modal form using table1.selectedRow.data
  • Build a confirmation dialog with Confirm and Cancel buttons wired to JS Queries
  • Close a modal from inside it using modal1.close() or from a button's Click event handler
  • Nest form components inside modals and access their data with form1.data after modal close
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 modals in Retool by adding a Modal component from the component panel. Open it with modal1.open() from a button click event handler or JS Query. Pass data into the modal by referencing table1.selectedRow.data inside modal components. Close it with modal1.close(). For confirmation dialogs, add Confirm and Cancel buttons that trigger the action query or close the modal.

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

Building Modal Dialogs and Confirmation Popups in Retool

Modals in Retool are fullscreen overlays — they appear above the main canvas and focus the user on a specific task before returning to the main view. Retool provides two modal-style containers: Modal (centered popup) and Drawer (slides in from left, right, top, or bottom). Both work the same way programmatically.

Modals are best used for: editing a record selected from a table, confirming a destructive action before it executes, displaying a create-new-record form, and showing expanded detail views. They are single-view popups — for multi-step sequential flows, use the wizard-interfaces approach instead.

This tutorial builds three common modal patterns: a simple 'Edit Record' modal that pre-fills from a table row, a confirmation dialog that gates a delete action, and a 'Create New' modal that resets its form on open.

Prerequisites

  • A Retool app with a Table component and data
  • Basic familiarity with event handlers and the Inspector panel

Step-by-step guide

1

Add a Modal component to the canvas

From the component panel, drag a Modal component onto the canvas. It appears as a grayed-out overlay placeholder in edit mode — its actual size and appearance are configured in the Inspector. Rename it to editModal in the Name field at the top of the Inspector. In the Inspector's General section, configure the Title ('Edit Customer'), Width (Small/Medium/Large), and whether to show/hide the default close X button. The modal content canvas is visible in the edit view — drag components directly onto it.

Expected result: A Modal component named editModal appears in the component tree. Its content area is accessible in edit mode.

2

Open the modal from a button or table row click

Add a Button to the main canvas (outside the modal) with label 'Edit'. In the Button's Inspector → Interaction → Event Handlers, add an event handler: Event = 'Click', Action = 'Control component', Component = editModal, Action = 'open'. Click the button — the modal opens as an overlay. Alternatively, add the handler to a Table component's Row action column: in the Table Inspector → Columns, add a button column and set its Click action to open the modal.

typescript
1// Button click event handler (Inspector → Interaction):
2// Event: Click
3// Action: Control component → editModal → open
4
5// Table row action button configuration:
6// In Table Inspector → Interaction → Row actions
7// Add button: label 'Edit', onClick → Control component → editModal → open
8
9// Open from a JS Query:
10modal1.open();
11// Or with additional context:
12await selectedRecord.setValue(table1.selectedRow.data);
13modal1.open();
14
15// Open a Drawer from the right side (same API):
16drawer1.open();

Expected result: Clicking the Edit button or table row action opens the modal as an overlay.

3

Pre-fill the modal form with the selected row's data

Inside the modal, add a Form component named editForm and add input fields: nameInput, emailInput, statusSelect. Set each input's Default Value to reference the corresponding field from table1.selectedRow.data. Since the modal opens when a row is selected, table1.selectedRow.data is available when the modal renders.

typescript
1// Default Value expressions for fields inside editModal:
2
3// nameInput Default Value:
4{{ table1.selectedRow.data.full_name }}
5
6// emailInput Default Value:
7{{ table1.selectedRow.data.email }}
8
9// statusSelect Default Value:
10{{ table1.selectedRow.data.status }}
11
12// createdAtText Value (read-only display):
13{{ new Date(table1.selectedRow.data.created_at).toLocaleDateString() }}
14
15// Hidden ID field (invisible, but accessible in form.data):
16// Add a Number Input named recordIdInput
17// Default Value: {{ table1.selectedRow.data.id }}
18// Hidden: true (in Layout section)

Expected result: Opening the modal with a table row selected shows the form pre-filled with that row's data.

4

Wire the Save button inside the modal

Add a Button inside the modal with label 'Save'. Create a JS Query named updateRecord. The query reads form data from editForm.data, calls the database update mutation, and closes the modal on success. In the Save button's Click event handler, trigger updateRecord. Also add a Cancel button with a Click handler that calls editModal.close() and editForm.reset().

typescript
1// JS Query: updateRecord
2// Trigger: Save button click
3
4const isValid = editForm.validate();
5if (!isValid) {
6 utils.showNotification({
7 title: 'Please fix the errors',
8 notificationType: 'warning',
9 });
10 return;
11}
12
13const data = editForm.data;
14
15try {
16 await updateCustomer.trigger({
17 additionalScope: {
18 id: data.recordIdInput,
19 fullName: data.nameInput,
20 email: data.emailInput,
21 status: data.statusSelect,
22 }
23 });
24
25 utils.showNotification({
26 title: 'Saved',
27 description: `${data.nameInput} updated successfully.`,
28 notificationType: 'success',
29 });
30
31 editModal.close();
32 await getCustomers.trigger(); // Refresh the table
33
34} catch (err) {
35 utils.showNotification({
36 title: 'Save failed',
37 description: err.message,
38 notificationType: 'error',
39 });
40 throw err;
41}

Expected result: Clicking Save validates the form, updates the record, closes the modal, and refreshes the table. Cancel closes without saving.

5

Build a confirmation dialog for destructive actions

Add a second Modal component named deleteConfirmModal. Give it a warning title: 'Delete Customer?' and add descriptive text: 'This action cannot be undone.' Add two buttons: 'Delete' (destructive, red styling) and 'Cancel'. Wire Delete to trigger a deleteCustomer JS Query. Wire Cancel to close the modal. Open deleteConfirmModal from the main table's Delete row action button.

typescript
1// deleteConfirmModal content:
2// Text component: 'Are you sure you want to delete {{ table1.selectedRow.data.full_name }}?\nThis action cannot be undone.'
3
4// Delete button event handler:
5// Click → Trigger query → deleteRecord
6
7// JS Query: deleteRecord
8try {
9 await deleteCustomer.trigger({
10 additionalScope: { id: table1.selectedRow.data.id }
11 });
12
13 utils.showNotification({
14 title: 'Deleted',
15 description: `${table1.selectedRow.data.full_name} has been removed.`,
16 notificationType: 'success',
17 });
18
19 deleteConfirmModal.close();
20 await getCustomers.trigger();
21
22} catch (err) {
23 utils.showNotification({
24 title: 'Delete failed',
25 description: err.message,
26 notificationType: 'error',
27 });
28 throw err;
29}
30
31// Cancel button event handler:
32// Click → Control component → deleteConfirmModal → close

Expected result: Clicking Delete on the table opens the confirmation modal. Confirming deletes the record and refreshes the table. Canceling closes without deleting.

6

Reset the modal form on open for a 'Create New' pattern

For a 'Create New' modal that should always start with an empty form, reset the form every time the modal opens. Instead of using a plain 'open' control action, trigger a JS Query from the button click that first resets the form, then opens the modal. This prevents previous user input from persisting when the modal is reopened.

typescript
1// JS Query: openCreateModal
2// Trigger: 'New Customer' button click
3
4// Reset the form before opening
5createForm.reset();
6
7// Clear any pre-filled values from defaults
8await createForm.setData({
9 nameInput: '',
10 emailInput: '',
11 statusSelect: 'active', // sensible default for new records
12 roleSelect: '',
13});
14
15// Open the modal
16createModal.open();
17
18// createModal save button JS Query:
19// JS Query: createRecord
20const data = createForm.data;
21const isValid = createForm.validate();
22
23if (!isValid) return;
24
25await insertCustomer.trigger({
26 additionalScope: {
27 fullName: data.nameInput,
28 email: data.emailInput,
29 status: data.statusSelect,
30 role: data.roleSelect,
31 }
32});
33
34utils.showNotification({ title: 'Customer created', notificationType: 'success' });
35createModal.close();
36await getCustomers.trigger();

Expected result: Clicking 'New Customer' opens the modal with a clean empty form every time, even if the modal was previously used.

Complete working example

JS Query: updateRecord
1// updateRecord — validates, saves, closes modal, refreshes table
2// Trigger: Save button inside editModal
3// Requires: editForm (Form component), editModal (Modal component)
4
5const isValid = editForm.validate();
6
7if (!isValid) {
8 utils.showNotification({
9 title: 'Form has errors',
10 description: 'Please fill in all required fields correctly.',
11 notificationType: 'warning',
12 });
13 return;
14}
15
16const data = editForm.data;
17
18// Safety check — ensure we have a record ID
19if (!data.recordIdInput) {
20 utils.showNotification({
21 title: 'Error',
22 description: 'Record ID is missing. Please close and reopen the edit form.',
23 notificationType: 'error',
24 });
25 return;
26}
27
28try {
29 await updateCustomer.trigger({
30 additionalScope: {
31 id: data.recordIdInput,
32 fullName: data.nameInput,
33 email: data.emailInput,
34 phone: data.phoneInput,
35 status: data.statusSelect,
36 notes: data.notesInput,
37 }
38 });
39
40 utils.showNotification({
41 title: 'Changes saved',
42 description: `${data.nameInput}'s profile has been updated.`,
43 notificationType: 'success',
44 });
45
46 editModal.close();
47
48 // Refresh the parent table
49 await getCustomers.trigger();
50
51} catch (err) {
52 utils.showNotification({
53 title: 'Save failed',
54 description: err.message || 'An unexpected error occurred.',
55 notificationType: 'error',
56 });
57 throw err;
58}

Common mistakes when creating Modal Dialogs in Retool

Why it's a problem: Opening a modal and expecting form Default Values to re-evaluate — Default Values are set at component mount, not at modal open time

How to avoid: Use form.setData() in the JS Query that opens the modal to explicitly set values from table1.selectedRow.data, rather than relying on Default Value expressions to update when the modal opens.

Why it's a problem: Not closing the modal after a successful save — the user sees no feedback that the action completed and is left with the modal still open

How to avoid: Always call modal1.close() in the success path of the save JS Query, after the notification. Then trigger a data refresh query to update the underlying table.

Why it's a problem: Adding the Cancel button's 'close modal' action directly in an event handler while the Save button triggers a JS Query that also closes the modal on success — this works fine, but adding an additional close handler to the Cancel path on top creates double-close errors

How to avoid: Keep close logic consistent: Cancel's Click → Control component → modal1 → close. Save's Click → JS Query → (close inside query on success). Do not mix event handler actions with JS Query close calls for the same modal.

Why it's a problem: Placing the Modal component inside a Container or other component — Retool requires Modal components to be at the root canvas level, not nested inside other containers

How to avoid: Drag the Modal component to the root of the canvas (not inside any Container, Tab, or other wrapper). Content placed inside the Modal's body can use any component types.

Best practices

  • Use descriptive modal names (editModal, deleteConfirmModal) — generic names like modal1 become confusing in apps with multiple modals
  • Add a hidden ID input inside edit forms and populate it with the record's ID — this makes update queries simple and reliable
  • Always validate the form before running the update query — call editForm.validate() and return early if it fails
  • Refresh the parent table after a successful create/update/delete by triggering the data query after modal.close()
  • Use a Drawer (slides in from the side) for complex edit forms that need more space — modals are for focused single-action dialogs
  • Add a keyboard shortcut (Enter to save, Escape to cancel) inside modals for power users — configure via Key press event handlers
  • Reset create-form modals before opening to prevent stale data from previous uses

Still stuck?

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

ChatGPT Prompt

I have a Retool app with a Table component (table1) showing customers with columns: id, full_name, email, status. I want to build an Edit modal: (1) add a Modal named editModal with a Form containing nameInput, emailInput, statusSelect, and a hidden recordIdInput, (2) pre-fill the form from table1.selectedRow.data when the modal opens, (3) write a JS Query named updateRecord that validates the form, calls the updateCustomer SQL query with the form data, closes the modal, and refreshes getCustomers. Also build a separate deleteConfirmModal with Confirm and Cancel buttons.

Retool Prompt

I have an editModal in my Retool app. When I click the Edit button, the modal opens but the form fields are empty instead of showing the selected row's values. The Default Value expressions reference table1.selectedRow.data.field_name. Why is this not working, and should I use form.setData() instead?

Frequently asked questions

What is the difference between a Modal and a Drawer in Retool?

Both are overlay containers that appear above the main canvas. A Modal is a centered popup dialog — good for confirmations and focused forms. A Drawer slides in from a screen edge (left, right, top, or bottom) — good for settings panels and complex edit forms that benefit from full height or width. Both use the same .open() and .close() API.

Can I prevent the modal from closing when the user clicks outside of it?

Yes. In the Modal component's Inspector → General, look for the 'Close on outside click' toggle and disable it. This forces users to use the explicit Close/Cancel button, which is appropriate for forms where accidental dismissal could lose work.

How do I open a modal from inside a Table row's action button?

In the Table Inspector → Interaction → Row actions, add a button column. Set the button's onClick action to 'Control component' → your modal → 'open'. The table row's data is available as table1.selectedRow.data inside the modal because clicking the row action also selects that row.

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.