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.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 15-20 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 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
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.
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.
1// Button click event handler (Inspector → Interaction):2// Event: Click3// Action: Control component → editModal → open45// Table row action button configuration:6// In Table Inspector → Interaction → Row actions7// Add button: label 'Edit', onClick → Control component → editModal → open89// Open from a JS Query:10modal1.open();11// Or with additional context:12await selectedRecord.setValue(table1.selectedRow.data);13modal1.open();1415// 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.
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.
1// Default Value expressions for fields inside editModal:23// nameInput Default Value:4{{ table1.selectedRow.data.full_name }}56// emailInput Default Value:7{{ table1.selectedRow.data.email }}89// statusSelect Default Value:10{{ table1.selectedRow.data.status }}1112// createdAtText Value (read-only display):13{{ new Date(table1.selectedRow.data.created_at).toLocaleDateString() }}1415// Hidden ID field (invisible, but accessible in form.data):16// Add a Number Input named recordIdInput17// 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.
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().
1// JS Query: updateRecord2// Trigger: Save button click34const isValid = editForm.validate();5if (!isValid) {6 utils.showNotification({7 title: 'Please fix the errors',8 notificationType: 'warning',9 });10 return;11}1213const data = editForm.data;1415try {16 await updateCustomer.trigger({17 additionalScope: {18 id: data.recordIdInput,19 fullName: data.nameInput,20 email: data.emailInput,21 status: data.statusSelect,22 }23 });2425 utils.showNotification({26 title: 'Saved',27 description: `${data.nameInput} updated successfully.`,28 notificationType: 'success',29 });3031 editModal.close();32 await getCustomers.trigger(); // Refresh the table3334} 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.
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.
1// deleteConfirmModal content:2// Text component: 'Are you sure you want to delete {{ table1.selectedRow.data.full_name }}?\nThis action cannot be undone.'34// Delete button event handler:5// Click → Trigger query → deleteRecord67// JS Query: deleteRecord8try {9 await deleteCustomer.trigger({10 additionalScope: { id: table1.selectedRow.data.id }11 });1213 utils.showNotification({14 title: 'Deleted',15 description: `${table1.selectedRow.data.full_name} has been removed.`,16 notificationType: 'success',17 });1819 deleteConfirmModal.close();20 await getCustomers.trigger();2122} catch (err) {23 utils.showNotification({24 title: 'Delete failed',25 description: err.message,26 notificationType: 'error',27 });28 throw err;29}3031// Cancel button event handler:32// Click → Control component → deleteConfirmModal → closeExpected result: Clicking Delete on the table opens the confirmation modal. Confirming deletes the record and refreshes the table. Canceling closes without deleting.
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.
1// JS Query: openCreateModal2// Trigger: 'New Customer' button click34// Reset the form before opening5createForm.reset();67// Clear any pre-filled values from defaults8await createForm.setData({9 nameInput: '',10 emailInput: '',11 statusSelect: 'active', // sensible default for new records12 roleSelect: '',13});1415// Open the modal16createModal.open();1718// createModal save button JS Query:19// JS Query: createRecord20const data = createForm.data;21const isValid = createForm.validate();2223if (!isValid) return;2425await insertCustomer.trigger({26 additionalScope: {27 fullName: data.nameInput,28 email: data.emailInput,29 status: data.statusSelect,30 role: data.roleSelect,31 }32});3334utils.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
1// updateRecord — validates, saves, closes modal, refreshes table2// Trigger: Save button inside editModal3// Requires: editForm (Form component), editModal (Modal component)45const isValid = editForm.validate();67if (!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}1516const data = editForm.data;1718// Safety check — ensure we have a record ID19if (!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}2728try {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 });3940 utils.showNotification({41 title: 'Changes saved',42 description: `${data.nameInput}'s profile has been updated.`,43 notificationType: 'success',44 });4546 editModal.close();4748 // Refresh the parent table49 await getCustomers.trigger();5051} 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation