Retool input validation has two layers: built-in rules configured in the Inspector (Required, Pattern, Min/Max) and JavaScript Custom Validation expressions. Control when errors appear by enabling 'Validate inputs on submit' on the Form component. Prevent submission with {{ form1.invalid }} on the Submit button's Disabled property. Use form1.validate() in JS Queries to force error display programmatically.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Intermediate |
| Time required | 20-25 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 2026 |
Validation UX Strategy for Production Retool Forms
Input validation in Retool operates at two levels: rule configuration (what is valid) and UX strategy (when to tell the user about errors). Most tutorials focus on the rule configuration — Required toggle, Email pattern, Custom Validation expressions. This tutorial focuses on the UX strategy: when should error messages appear, how do you prevent bad submissions, and how do you guide users toward valid input without frustrating them?
The key decisions are: (1) do you show errors on blur (when the user leaves a field) or only on submit attempt? (2) do you disable the submit button immediately or let users click it to see all errors at once? (3) how do you combine built-in rules with custom JavaScript validators without creating confusing error stacking?
This tutorial complements the validation-in-retool-forms tutorial (which covers rule configuration) and the custom-validators tutorial (which covers JavaScript Custom Validation). It covers how to wire them together for a polished user experience.
Prerequisites
- Familiarity with built-in validation rules from the validation-in-retool-forms tutorial
- A Form component with multiple input fields including Required and Pattern rules
- Basic understanding of Temporary State variables and setValue()
Step-by-step guide
Choose between on-blur and on-submit error display
Retool shows validation errors on blur (when the field loses focus) by default. This provides immediate feedback but can overwhelm users who are still filling out the form. For shorter forms (3-5 fields), on-blur works well. For longer forms, enable 'Validate inputs on submit' on the Form component in Inspector → General. This suppresses all error messages until the user first clicks Submit, then shows all errors simultaneously.
Expected result: With 'Validate inputs on submit' enabled, no errors show while filling out the form. They appear only after the first submit click.
Disable the Submit button with form1.invalid
Select the Submit button inside the Form. In Inspector → Interaction → Disabled, enter {{ form1.invalid }}. This greys out the button whenever any required field is empty or any validation rule fails. The button activates only when all rules pass. Add a Tooltip to explain why the button is disabled: in Inspector → General → Tooltip, enter {{ form1.invalid ? 'Please fill in all required fields' : '' }}.
1// Submit button Inspector settings:2// Disabled: {{ form1.invalid }}3// Tooltip: {{ form1.invalid ? 'Please fill in all required fields' : 'Click to submit' }}45// To only enable after a specific step in a multi-step form:6// Disabled: {{ form1.invalid || currentStep.value < 3 }}78// To add a visual indicator of remaining errors:9// Button text: {{ form1.invalid ? `Submit (${errorCount.value} errors)` : 'Submit' }}Expected result: Submit button is greyed out on load. It activates automatically when all validation rules pass.
Use individual component.invalid for conditional behavior
Beyond the form-level form1.invalid, each input component exposes its own .invalid property. Use this to conditionally style related components or show field-specific guidance. For example, show a helper text component only when a specific field has an error, or change a container's border color based on a field's validity state.
1// Show a help text only when emailInput is invalid:2// Text component Hidden property:3{{ !emailInput.invalid }}45// Container border color (via inline style) based on validity:6// Style expression: {{ emailInput.invalid ? 'border: 2px solid red' : 'border: 2px solid #e2e8f0' }}78// Disable a downstream action button until a specific field is valid:9// Disabled: {{ usernameInput.invalid || usernameInput.value === '' }}1011// Count total invalid fields:12{{ [nameInput, emailInput, phoneInput, roleSelect].filter(c => c.invalid).length }}Expected result: Help text appears only when emailInput is invalid. Error count badge updates reactively as fields are corrected.
Implement a 'formSubmitted' flag for deferred error display
For the best UX in forms without the 'Validate inputs on submit' toggle (e.g., when using individual inputs outside a Form component), implement your own formSubmitted flag. Create a Temporary State variable named formSubmitted with default value false. Make input error messages visible only when formSubmitted.value is true. Set formSubmitted to true when the Submit button is first clicked. This replicates the 'Validate inputs on submit' behavior for non-Form component layouts.
1// Temporary State: formSubmitted (default: false)23// Error Text component Hidden property:4// Show email error only after first submit attempt:5{{ !formSubmitted.value || !emailInput.invalid }}67// JS Query: handleSubmit8await formSubmitted.setValue(true);910// Check manually since we're not using Form component:11const isValid = !nameInput.invalid12 && !emailInput.invalid13 && !phoneInput.invalid14 && roleSelect.value !== null;1516if (!isValid) {17 utils.showNotification({18 title: 'Please fix the errors',19 notificationType: 'warning',20 });21 return;22}2324// Proceed with submission...25await insertRecord.trigger();2627// Reset the flag for next use28await formSubmitted.setValue(false);Expected result: No error messages show on load. After the first Submit click, all invalid fields reveal their error messages simultaneously.
Layer built-in rules and custom validators effectively
Combine built-in rules (Required, Email pattern, Min Length) with Custom Validation expressions to create progressive validation. The built-in rules run first. Only if they pass does the Custom Validation expression evaluate. This means a malformed email does not trigger the uniqueness check — the user sees the format error first and fixes it before the uniqueness check runs. Stack them purposefully: generic constraints as built-in, business logic as custom.
1// Layered validation strategy for emailInput:2// Layer 1 (built-in) — Required: ON, error: 'Email is required'3// Layer 2 (built-in) — Pattern: Email, error: 'Enter a valid email address'4// Layer 3 (custom) — Custom Validation expression:5// {{ getExistingEmails.data.some(r => r.email === self.toLowerCase()) 6// ? 'This email is already registered' : '' }}78// Result: user sees errors progressively:9// Empty → 'Email is required'10// Bad format → 'Enter a valid email address'11// Valid but taken → 'This email is already registered'12// Valid and unique → no error, field contributes to form.validExpected result: Users are guided through validation errors progressively — format errors before uniqueness errors, simplifying the correction flow.
Show a global error summary on failed submission
For long forms, supplement inline errors with a dismissible error summary at the top. Create a Container component at the top of the form with a bulleted list of invalid field names. Use the formSubmitted flag and individual .invalid properties to build the list. This helps users on long forms who may not see inline errors below the fold.
1// Error summary Text component content:2{{ formSubmitted.value && form1.invalid 3 ? 'Please fix the following:\n' + [4 nameInput.invalid ? '• Name is required' : '',5 emailInput.invalid ? '• Valid email address required' : '',6 phoneInput.invalid ? '• Valid phone number required' : '',7 ].filter(Boolean).join('\n')8 : '' }}910// Error summary Container Hidden property:11{{ !formSubmitted.value || !form1.invalid }}1213// Alternative: use utils.showNotification for a toast:14if (invalidCount > 0) {15 utils.showNotification({16 title: `${invalidCount} field${invalidCount > 1 ? 's' : ''} need attention`,17 description: 'Scroll up to see highlighted errors.',18 notificationType: 'warning',19 });20}Expected result: An error summary appears at the top of the form after a failed submit attempt, listing all invalid fields.
Complete working example
1// handleFormSubmit — complete validation-aware submission handler2// Form component: form13// Temporary State: formSubmitted (default: false)45// Mark as submitted to show all error messages6await formSubmitted.setValue(true);78// Force all inline error messages to display9const isValid = form1.validate();1011if (!isValid) {12 // Count invalid fields for the notification13 const inputs = [nameInput, emailInput, phoneInput, roleSelect, departmentSelect];14 const invalidCount = inputs.filter(c => c.invalid).length;1516 utils.showNotification({17 title: 'Form has errors',18 description: `${invalidCount} field${invalidCount > 1 ? 's' : ''} need${invalidCount === 1 ? 's' : ''} correction.`,19 notificationType: 'warning',20 });2122 return;23}2425// All validation passed — submit26try {27 await createRecord.trigger({28 additionalScope: {29 name: form1.data.nameInput,30 email: form1.data.emailInput,31 phone: form1.data.phoneInput,32 role: form1.data.roleSelect,33 department: form1.data.departmentSelect,34 }35 });3637 utils.showNotification({38 title: 'Record created',39 description: `${form1.data.nameInput} has been added successfully.`,40 notificationType: 'success',41 });4243 // Reset form and submission flag44 form1.reset();45 await formSubmitted.setValue(false);4647} catch (err) {48 utils.showNotification({49 title: 'Submission failed',50 description: err.message,51 notificationType: 'error',52 });53 throw err;54}Common mistakes when handling User Input Validation in Retool
Why it's a problem: Using {{ !form1.valid }} instead of {{ form1.invalid }} for the Disabled expression — form1.valid does not exist; the property is form1.invalid
How to avoid: Use {{ form1.invalid }} on the Disabled property. The inverse is {{ !form1.invalid }} if you need to check validity.
Why it's a problem: Enabling 'Validate inputs on submit' on the Form component AND binding Disabled to {{ form1.invalid }} on the Submit button — the button is disabled so the submit action never fires to trigger the validation display
How to avoid: When using 'Validate inputs on submit', either remove the Disabled binding (let users click the button to see errors) OR keep Disabled but make errors show on blur. The two approaches are mutually exclusive.
Why it's a problem: Calling form1.validate() in a submit query but not awaiting it — validate() is synchronous but the UI re-render for error display may not complete before you read form1.invalid
How to avoid: Call form1.validate() and immediately check its return value: const isValid = form1.validate(); if (!isValid) return;. The return value is reliable; the UI display lags behind but the logic is correct.
Why it's a problem: Placing the Submit button outside the Form component and expecting form1.invalid to work — form1.invalid only includes components inside the Form
How to avoid: Either move the Submit button inside the Form component, or manually build an isValid expression using individual component.invalid properties: {{ nameInput.invalid || emailInput.invalid }}.
Best practices
- Choose one error display strategy and apply it consistently: either 'on blur' for short forms or 'on submit' for longer ones — mixing creates confusing UX
- Always combine a disabled Submit button (form1.invalid) with error messages — the disabled button sets user expectations before they try to submit
- Use progressive validation layering: built-in rules for format/required → Custom Validation for business logic — users see the most actionable error first
- Provide positive feedback (green checkmark or border change) when a field becomes valid, not just negative feedback when invalid
- For accessibility, ensure error messages are associated with their input fields using the error message fields in the Inspector rather than separate Text components
- Reset the formSubmitted flag after successful submission so the next use of the form starts clean
- Avoid showing more than 3-4 error conditions per field — if a field has many rules, document the requirements in the label or tooltip rather than stacking errors
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Retool registration form (form1) with these inputs: nameInput (required), emailInput (required, email format, must be unique against getExistingEmails.data), phoneInput (required, US format), roleSelect (required). I want: (1) errors to appear only after the first submit attempt using 'Validate inputs on submit', (2) Submit button disabled while form1.invalid is true, (3) a JS Query handleFormSubmit that calls form1.validate(), shows a count of invalid fields in a notification if invalid, and calls createRecord on success. Write the complete solution including the JS Query and Inspector configuration notes.
My Retool form's Submit button is disabled because form1.invalid is true, but I have already filled in all the required fields. How do I debug which field is still invalid? Is there a way to see all the validation states at once in Retool?
Frequently asked questions
How do I show a green checkmark icon when a field is valid instead of just removing the red error?
Add an Icon component (or use a Text component with a checkmark character ✓) next to the input field. Set its Hidden property to {{ !nameInput.value || nameInput.invalid }} so it only shows when the field has a value AND is valid. This requires coordinating Hidden expressions for both the error state and the success state separately.
Can I validate a field against a value the user has not finished typing yet (e.g., check uniqueness on every keypress)?
Built-in rules and Custom Validation expressions run on every change, which includes every keypress. For async uniqueness checks via query triggers, add a debounced Change event handler (300ms debounce) that triggers the check query. Then reference the query result in the Custom Validation expression. Without debouncing, a uniqueness check fires a database query on every single character typed.
Does validation work on components inside a Modal or Drawer?
Yes — components inside a Modal or Drawer are part of the same app and expose the same .invalid properties. If those components are inside a Form component in the modal, form1.invalid aggregates their state normally. However, validation error messages may not be visible if the modal is closed — ensure validation is checked while the modal is open before dismissing it.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation