WeWeb's Form Container component manages form state and validation automatically — it exposes formData, isValid, and per-field validation states. Use built-in rules (required, email, min/max, regex) for standard cases and custom validation formulas for business logic. This tutorial covers single-step and multi-step forms, file uploads, disabling submit on invalid state, and connecting submissions to Supabase or a REST API.
Form Validation and Multi-Step Forms in WeWeb
WeWeb's Form Container is the purpose-built component for all form work — it automatically collects input values into a formData object, tracks per-field and form-level validity, and fires an On submit event you wire to your backend. This tutorial covers the complete form workflow: configuring built-in validation rules, writing custom validation formulas for business logic (password confirmation, conditional required fields, cross-field validation), disabling the submit button reactively, showing inline error messages without custom JavaScript, building a multi-step form using a step counter variable, handling file uploads, and writing form data to Supabase or a REST API on submit.
Prerequisites
- A WeWeb project with at least one page
- Optional: a Supabase project connected via the backend plugin for storing submissions
- Optional: a REST API endpoint for form submissions
- Basic familiarity with WeWeb variables and workflow actions
Step-by-step guide
Add a Form Container and understand its exposed properties
Add a Form Container and understand its exposed properties
In the left panel, click Add element (+) → Form Elements → drag Form Container onto your canvas. The Form Container is a special wrapper that tracks all Input, Select, Checkbox, Toggle, and Radio Group elements placed inside it as children. When you drop the Form Container, it creates a form scope. Any form element you add inside it is automatically registered and its value included in formData. Select the Form Container → Settings panel. You see two key settings: Validation mode ('On input change' for real-time validation as the user types, or 'On submit' for validation only when submit is clicked). For most forms, 'On input change' with submit disabled while invalid provides the best UX. The Form Container exposes these bindable properties: formData (object containing all field values), isValid (Boolean — true only when all required fields pass their rules). Individual inputs also expose their own isValid. You reference these in bindings as: formContainer['formData'] and formContainer['isValid'].
Expected result: A Form Container is on the page. You understand that formData and isValid are the two core properties driving form behavior.
Add inputs and configure built-in validation rules
Add inputs and configure built-in validation rules
Inside the Form Container, add Input elements from Add element (+) → Form Elements → Input. For each input: select it → Settings panel → set the Name field (this becomes the key in formData — e.g., 'email' means formData.email contains its value). Set Input type to match the data: 'email' for email addresses, 'password' for passwords, 'number' for numeric fields. In the Validation section of the input Settings panel, configure rules: Required (checkbox — form is invalid if this field is empty), Email format (available when type is 'email' — validates format), Min length (minimum character count), Max length (maximum character count), Pattern (regex for custom format, e.g., `^[0-9]{5}$` for US zip codes). Each rule has an error message field — enter a user-friendly message like 'Please enter a valid email address'. Multiple rules can be active on a single input — all must pass for the field to be valid. The input's border changes color automatically based on its validity state when validation mode is 'On input change'.
Expected result: Your form has named inputs with validation rules configured. Entering invalid data shows the error messages you configured below each field.
Write custom validation formulas for business logic
Write custom validation formulas for business logic
Built-in rules cover common cases. For business logic — password confirmation, conditional required fields, cross-field validation — use custom validation formulas. Select an input → Settings panel → Validation section → toggle on 'Custom validation' → click the plug icon next to the validation formula field. Write a formula that returns true when the value is valid and false when it is not. Examples: Password confirmation — `formContainer['formData'].password === formContainer['formData'].confirm_password`. Minimum age — `new Date().getFullYear() - new Date(formContainer['formData'].birth_date).getFullYear() >= 18`. Phone number format — `/^\+?[1-9]\d{7,14}$/.test(formContainer['formData'].phone)`. Custom error message for failed custom validation: enter it in the 'Custom validation error message' field below the formula. Custom validation runs in addition to built-in rules — both must pass for the field to be valid. Custom validation formulas have access to all formData values, so cross-field comparisons work correctly.
1// Injection point: Input Settings panel → Validation → Custom validation formula2// These are example formulas to paste into the binding formula editor34// Password confirmation match:5formContainer['formData'].password === formContainer['formData'].confirm_password67// At least one checkbox in a group checked:8formContainer['formData'].interests && formContainer['formData'].interests.length > 0910// URL format validation:11/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)$/.test(formContainer['formData'].website_url)1213// Conditional required: company name required only if 'business' account type selected:14formContainer['formData'].account_type !== 'business' || (formContainer['formData'].company_name && formContainer['formData'].company_name.length > 0)1516// Number range validation:17formContainer['formData'].quantity >= 1 && formContainer['formData'].quantity <= 100Expected result: Cross-field validation (like password confirmation) works correctly. The custom error message appears when the formula returns false.
Disable the submit button when the form is invalid
Disable the submit button when the form is invalid
Select the Submit Button inside the Form Container → Styling panel → find the Disabled state or use the Settings panel → bind the 'Disabled' property to: `not(formContainer['isValid'])`. This binding makes the button unclickable when the form has validation errors. For visual feedback, style the disabled button differently: in Styling panel → create a subclass for ':disabled' state → set opacity: 0.5 and cursor: not-allowed. Now the button is visually dimmed and non-interactive until all required fields pass validation. An alternative approach — allow clicking but validate on submit: change the Form Container's validation mode to 'On submit' and do not bind the button's disabled state. This shows errors only after the first submit attempt, which some UX patterns prefer (less aggressive inline validation). For inline error messages under each field: select the input → Settings panel → Error message display — toggle on 'Show error message'. This automatically shows the configured error text below the input when validation fails.
Expected result: The submit button is visually disabled and unclickable until all form fields pass validation. Error messages appear below invalid fields.
Connect form submission to Supabase or REST API
Connect form submission to Supabase or REST API
Select the Form Container → Workflows tab → On submit trigger → add actions. For Supabase: add action → Supabase → Database Insert → select your table → in the Data field, bind to `formContainer['formData']` if your field names match column names exactly. If column names differ, use an Object formula: `{name: formContainer['formData'].full_name, email: formContainer['formData'].email_address, message: formContainer['formData'].message_text}`. After the insert action, add: Change variable value → a Boolean 'formSubmitted' → true. Then bind the Form Container's display to `not(formSubmitted)` and show a success message bound to `formSubmitted`. For error handling: add an On error action branch → Change variable value → 'formError' string → the error message → bind an error message text element's display to `formError.length > 0`. For REST API: add action → REST API Request → Method: POST → URL: your endpoint → Body (JSON): `JSON.stringify(formContainer['formData'])` or a mapped object.
1// Injection point: Supabase SQL editor (run OUTSIDE WeWeb)2// Create the contacts table for form submissions34CREATE TABLE IF NOT EXISTS public.contacts (5 id UUID DEFAULT gen_random_uuid() PRIMARY KEY,6 full_name TEXT NOT NULL,7 email TEXT NOT NULL,8 company TEXT,9 message TEXT NOT NULL,10 created_at TIMESTAMPTZ DEFAULT now()11);1213ALTER TABLE public.contacts ENABLE ROW LEVEL SECURITY;1415-- Allow anonymous users to submit contact forms16CREATE POLICY "Public can insert contact forms"17 ON public.contacts18 FOR INSERT19 WITH CHECK (true);2021-- Only authenticated users (admins) can read submissions22CREATE POLICY "Auth users can read contacts"23 ON public.contacts24 FOR SELECT25 USING (auth.role() = 'authenticated');Expected result: Submitting the form writes a record to your database. A success message replaces the form. An error message appears if the submission fails.
Build a multi-step form with a step counter
Build a multi-step form with a step counter
Multi-step forms break a long form into digestible sections. WeWeb's approach: create a Number variable for the current step. Go to Data panel → Variables → New → Number → name it 'currentStep' → default value 1. Build each step as a separate container. Use Conditional Rendering on each step container: Step 1 container → Conditional Rendering → `currentStep === 1`. Step 2 container → Conditional Rendering → `currentStep === 2`. Step 3 container → Conditional Rendering → `currentStep === 3`. Each step container holds its own form fields. Add 'Next' and 'Back' buttons: Next button workflow: Validate current step first (optionally add a custom check), then Change variable value → currentStep → `currentStep + 1`. Back button workflow: Change variable value → currentStep → `currentStep - 1`. Disable the Next button when the current step's required fields are invalid. Add a progress indicator: a row of dots or numbers whose styling is bound to the currentStep variable. Wrap ALL step containers inside ONE Form Container so formData accumulates across all steps and the final submit has all data.
Expected result: The form advances through steps when Next is clicked, Back returns to the previous step, and the final submit sends all accumulated data.
Handle file upload inputs
Handle file upload inputs
For forms that include file uploads (profile photos, document attachments): drag a File Upload element inside the Form Container. In its Settings panel: set Accepted file types (e.g., 'image/*' for images, '.pdf,.doc,.docx' for documents), Maximum file size (in bytes — 5000000 = 5MB), and whether multiple files are allowed. The File Upload element exposes the selected file(s) as a bindable property. For uploading to Supabase Storage: the workflow on form submit requires two steps. Step 1: Add action → Supabase → Storage Upload file → Bucket: your bucket name → File: bind to the File Upload element's file property → Path: `uploads/${formContainer['formData'].full_name}_${Date.now()}`. Step 2: The Storage upload action returns the file URL. Add action → Supabase → Database Insert → include the returned file URL in the data object alongside other form fields. NOTE: this step requires a separate variable to hold the upload result between the two workflow actions. Create a variable 'uploadedFileUrl' and use Change variable value → bind to the upload result URL between the two steps.
Expected result: File uploads go to Supabase Storage. The resulting file URL is stored alongside the other form data in the database.
Complete working example
1// Injection point: Workflow → Custom JavaScript action2// Use this in a 'Validate current step' action before advancing to the next step3// This checks that all required fields in the current step are filled45// Access the form data via the WeWeb workflow context6// Replace field names with your actual input names78const step = variables.currentStep;9const formData = variables.formContainer_formData; // or reference via context1011let isStepValid = false;1213switch (step) {14 case 1:15 // Step 1 requires: full_name and email16 isStepValid = 17 formData.full_name && formData.full_name.trim().length > 0 &&18 formData.email && /^[^@]+@[^@]+\.[^@]+$/.test(formData.email);19 break;2021 case 2:22 // Step 2 requires: company and role23 isStepValid = 24 formData.company && formData.company.trim().length > 0 &&25 formData.role && formData.role.length > 0;26 break;2728 case 3:29 // Step 3 requires: message with at least 20 chars30 isStepValid = 31 formData.message && formData.message.trim().length >= 20;32 break;3334 default:35 isStepValid = true;36}3738if (!isStepValid) {39 // Return false — a subsequent Branching action can check this40 // and either advance the step or show an error41 return { canAdvance: false };42}4344return { canAdvance: true };Common mistakes
Why it's a problem: Placing form inputs outside the Form Container, so their values are not included in formData
How to avoid: Every input that should be part of the form submission must be a direct or nested child of the Form Container element. Inputs outside the Form Container are not registered and will not appear in formData. If you need to visually separate inputs across different layout containers, those layout containers must still be inside the Form Container.
Why it's a problem: Not naming inputs in the Settings panel, causing formData to have empty or auto-generated keys
How to avoid: Every Input, Select, Checkbox, and other form element has a 'Name' field in its Settings panel. Set this to a descriptive, database-matching name (e.g., 'email', 'company_name'). Without an explicit name, WeWeb auto-generates an ID-based name that does not match your database columns.
Why it's a problem: Using multiple Form Containers for a multi-step form, losing Step 1 data by Step 3
How to avoid: Use a single Form Container wrapping all steps. Use conditional rendering to show/hide individual step containers. This keeps all form field values in a single formData object accessible throughout all steps and on final submission.
Why it's a problem: Binding the submit button's disabled state to the form's isValid before the user has typed anything, showing an already-disabled button on page load
How to avoid: Consider changing the Form Container validation mode to 'On submit' for the initial experience. After the first submit attempt, switch to 'On input change' by storing a Boolean variable 'formSubmitAttempted'. Bind validation mode to this variable so real-time feedback only activates after the first submission attempt — less aggressive and more user-friendly.
Best practices
- Always set the Name property on every form input to match your database column names — this lets you pass formData directly to Supabase insert actions
- Use 'On input change' validation mode with the submit button bound to not(formContainer['isValid']) for the best UX on simple forms
- Add explicit success and error states — replace the form with a success message and show an error message if the backend insert fails
- For multi-step forms, use a single Form Container wrapping all steps and a step counter variable to show/hide step containers
- Test your form on mobile — ensure all inputs have minimum touch height of 44px and that the keyboard does not obscure submit buttons
- Protect your backend with RLS policies (Supabase) or API authentication — never rely on the frontend form alone to prevent malicious submissions
- Add a honeypot field (a hidden input that legitimate users never fill) as a basic spam filter for public contact forms
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a contact form in WeWeb using the Form Container component. The form has: full_name (required, min 2 chars), email (required, valid email format), company (optional), message (required, min 20 chars). After submission, I need to insert the data into a Supabase table called 'contacts'. What validation rules should I configure, what Supabase RLS policy do I need, and how should I handle the success/error states in the WeWeb workflow?
In WeWeb, I'm building a 3-step registration form. All steps are inside one Form Container. Step 1 has: name, email. Step 2 has: company, role, company_size (select). Step 3 has: password, confirm_password (custom validation: must match). I have a variable 'currentStep' starting at 1. What bindings do I use on the Next button's disabled state, what conditional rendering formula do I use on each step container, and how do I wire the final submit to create a Supabase Auth user?
Frequently asked questions
How do I show a real-time character count under a textarea in WeWeb?
Add a Text element below the textarea input. Bind its text content to a formula: `formContainer['formData'].message_field_name.length + ' / 500 characters'` (replace 'message_field_name' with your input's Name value and 500 with your maximum). The count updates as the user types because the binding is reactive to formData changes. Style the counter red when over the limit: bind its color to `if(formContainer['formData'].message_field_name.length > 500, '#ef4444', '#6b7280')`.
Can I pre-fill WeWeb form fields with data from a URL parameter or logged-in user?
Yes. Set the Default value property on each Input element in the Settings panel. Bind the default value to: a Query variable (for URL parameter pre-fill, e.g., query.email), wwContext.user.email (for the logged-in user's email from any auth plugin), or a Page-level variable that you populate on page load from a collection. Pre-filled values are included in formData immediately, before the user types anything.
How do I reset a WeWeb form after successful submission?
Add a Reset variable action in the On submit workflow after the success action. Or add a dedicated 'Reset form' button that triggers a workflow: select the Form Container → Workflows → add a custom trigger → action: Reset variable → target each input's value variable individually. The cleanest approach is to use the Form Container's 'Reset form' workflow action if available in your WeWeb version, or conditionally render the Form Container bound to a 'formSubmitted' Boolean — setting it true hides the form, and a 'Submit another' button sets it back to false.
Is WeWeb form data sent securely when submitted to Supabase?
WeWeb communicates with Supabase over HTTPS — data in transit is encrypted. However, because WeWeb generates a frontend SPA, all form data is processed in the user's browser. The Supabase anon key is visible in browser dev tools, which is expected — it is a public key. Security comes from your Supabase RLS policies: a well-configured INSERT policy allows submissions while blocking unauthorized reads. Never allow SELECT on tables with sensitive form submissions unless the reading user is authenticated and authorized via RLS.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation