Bootcamp
Build a create form with validation in Bubble
Photo by Tim Schmidbauer on Unsplash
Every database record starts life in a form. Before your app can list, sort, filter or report on anything, someone has to type data in and press save, and the quality of that ten-second interaction decides the quality of everything downstream. A form with the wrong input types or no validation quietly fills your database with rubbish you’ll be cleaning up for months.
In Bubble, saving form data is the C in CRUD, and it comes down to one workflow action. Here’s the full build: a new-contact form for a CRM, from data type to working save button.
Create a new thing is the Bubble workflow action that writes a new record to the database. You trigger it (usually from a Save button), choose the data type, and map each of its fields to an input’s value.
Define the data type first
Before a form can save anything, there has to be somewhere for the data to go. In the Data tab, create a new type called Contact, then add its fields. The field types deserve a moment’s thought, because they’re easy to get wrong:
- First name and Last name are text.
- Colour is text. We’re letting users assign a colour to each contact, and colours are stored as hexadecimal codes, which are text strings.
- Email is text.
- Phone is text too, and this is the one that trips people up. A phone number is not a number you’ll ever do arithmetic on. Store it as a number type and you lose the leading zero and the ability to include spaces and characters. Text keeps it exactly as entered.
- Location is a geographic place. This type stores the place with its coordinates and retrieves the text value from the Google Maps API.
- Profile picture is an image.
A tip for speed: rather than scrolling the field-type dropdown, search within it. Once you’re used to searching everywhere in the Bubble editor, you move much faster.
You’ll also need a page to host the form. Consult your application architecture: in this CRM there’s a Contacts page for the list, with a Contact page sitting under it for a single record. Add the new page, name it contact, and clone it from your page template so the shell, header and styles come across for free.
Build the form
Now the layout. This is the longest part of any create flow, and doing it with proper container structure pays off when the page needs to be responsive.
- Replace the content placeholder with a container group and name it Form Wrapper. Make it a column, remove the fixed width and minimum height, and don’t fit width to content. Give it a white background, an 8-pixel corner radius and 48 pixels of padding, matching the design. Setting the padding on the wrapper itself means the gap survives even if the page ends up narrower than expected; there’s nothing worse than losing your breathing room the moment a page gets crushed.
- Add the breadcrumb row above the wrapper: a text element in your default regular style (strip the fit-width and fit-height defaults so it behaves), a 24 by 24 Material icon for the back arrow (fixed size), and an “Add contact” text in the medium-bold weight and primary 500 colour. Select all three, right-click, and group elements into a row container. The icon’s fixed 24-pixel width gives the row its spacing on its own.
- Add the heading row: a “New contact details” text in your subheading style, plus Cancel and Save buttons. Fix the button height at 40, fit width to content, and sort the type out properly: if your design has large, medium and small button variants, this is the medium. Create the style you’re missing (a primary button medium) rather than detaching styles ad hoc, and make Cancel a duplicate of your secondary style. Group the two buttons into a row container named Group Right with a column gap of 20, then group the heading and Group Right into a Heading Row using the space-between alignment so the title sits left and the buttons sit right.
- Move Group Breadcrumbs and Heading Row out of the Form Wrapper and into the page’s content group, with a row gap of 32 on the content group. The wrapper should hold only the form itself; that’s what it’s for.
- Build the profile picture uploader: a picture uploader element named Profile Pic, 224 by 224, fixed. To round a square into an exact circle, set the corner radius to half the width: 112 pixels. Fill it with your off-white shade and give it a grey-200 border. (If the design uses a dashed border spacing Bubble can’t reproduce exactly, get it close and move on.)
- Lay out the fields in rows inside a Form Content group sitting beside the uploader, with a gap of 48 between the two. Cap the form at a max width of 600, not a min width; max keeps it tidy on wide screens while still letting it shrink. Reuse a labelled field group you’ve already built elsewhere if you have one; select the group by its wrapper in the elements tree, copy, and paste it in.
- Row one: First name, Last name, and the colour picker, grouped into a row container with a column gap of 24. Change the labels and, importantly, the input names to match. Do this every time; a workflow editor full of inputs all named “Input A” is unreadable.
- The colour field looks like a dropdown in the design, but what it actually needs is a colour picker, which Bubble doesn’t ship natively. Install a colour picker from the plugin library (search with the American spelling, “color”; a free zero-code option works fine), drop the Air Color Picker element in, and fix its height at 40 to match your inputs.
- Row two: Email and Phone, another row container with the same 24 gap. Set the email input’s content format to email and the phone input’s to text.
- Row three: Location. A plain input won’t do here. Use a Search Box element with its choices style set to geographic places, height fixed at 40. It autocompletes addresses via the Google Maps API. It can be a little hit and miss while you type, but it gets there.
- Set a row gap of 24 on the form content column so the rows space evenly, and clear any temporary background colours you used while positioning.
If you lose track of what’s nested where (and with wrappers inside wrappers, you will), open the elements tree. Naming repeated groups Group Row is fine until you have five hundred of them; name anything you’ll ever need to search for.
Validate the data
Validation in Bubble largely happens on the input elements themselves, so walk every field before wiring the workflow:
- Picture uploader: limit the file size. Bubble’s default is reasonable; the point is to have checked it.
- Mandatory fields: on first name, last name, email and phone, tick This input should not be empty. That single checkbox is Bubble’s required-field validation, and it blocks the save workflow until the field is filled.
- Content formats: email as email format, phone as text. The format setting is what makes Bubble reject “not-an-email” for you.
- Placeholders: replace the default “type here” placeholders with something helpful where it matters.
- Location: you can make it mandatory or not, your call. If Google’s autocomplete is being fussy for your test data, you can also insert a value dynamically.
Then do the unglamorous check that saves you an hour later: make sure every input has a unique, descriptive name. Two inputs both called “Email” will be very annoying indeed once you’re mapping fields. Rename the wrapping groups to match (Group First Name, Group Last Name, Group Colour, Group Email, Group Phone, Group Location) so the elements tree reads cleanly.
Wire up the Create workflow
The preparation was the hard part. The workflow is short.
- Select the Save button and add a workflow. (With Show element labels in view turned on, buttons without workflows get a little yellow marker, which is a handy audit tool in itself.) This creates the event When Button Save is clicked.
- Add an action: Data → Create a new thing, and choose Contact as the type.
- Now map the fields, which is the whole point. One rule matters here: you don’t map a field to the input, you map it to the input’s value.
Input First Namealone won’t work;Input First Name's valueis the typed content. So: First name =Input First Name's value, Last name =Input Last Name's value, Email =Input Email's value, Phone =Input Phone's value, Colour =Air Color Picker's value, Location =SearchBox Location's value. - To find each input quickly on a busy page, type “input” into the expression builder and pick from the list rather than scrolling. And if you spot a naming mistake while you’re here, fix it at the element; the corrected name flows through to the workflow automatically.
- Sanity-check the types. Hover each expression and confirm what it evaluates to: the text fields should evaluate to text, the location to a geographic address. If an expression’s type doesn’t match the field’s type, it goes red, and that’s your cue to debug before previewing.
Then test it properly. Preview the page, fill in the form, upload an image, hit Save, and go to Data → App data → All Contacts in the editor to inspect the record. On the first run of this build, the record came back with every field populated except the profile picture, because that one mapping had been missed in the action. That’s not a sign of a bad tutorial; that’s Bubble development. You will always make mistakes and have to fix them, which is exactly why the test-then-check-the-database loop is non-negotiable. Add the missing line (Profile picture = Picture Uploader's value), refresh, save again, and confirm the second record is complete.
Try it yourself
Build the form end to end, then add three or four more contacts through it in preview mode, checking App data after each save. Deliberately leave a mandatory field blank once to watch the validation hold the door, and try a phone number with a leading zero to see why that field is text. A contacts database with real-looking rows in it is also exactly what you’ll want when you build the table that reads this data back out.
This skill in the AI era
A create form is the pattern AI coding tools reproduce a thousand times a day, and the judgement in this article is precisely what you still supply: which fields are mandatory, why phone numbers are text, what the input’s value actually is, and the discipline of saving a record and then going to look at it in the database. When we direct Claude Code on a build, those decisions are the prompt and the review checklist; the machine handles the markup. Teams pick this up fastest with structured, hands-on practice, which is what our Claude Code training in Sydney is built around. The longer arc of what carried over from Bubble to AI-assisted development starts with fundamentals like this one.
If this sparked something, let's talk.
No pitch, no pressure — just a conversation about what you're working on.
Let's talkRelated posts
Bootcamp
A simple approach to data modelling
A four-step method for designing a database schema: identify entities, map relationships, choose fields. Taught through the CRM data model built in Bubble.
Bootcamp
Module 5 — Application architecture: pages, states and reusable pieces
Single-page vs multi-page apps, URL parameters, custom states, reusable elements — and a big practical building a main template with an expanding navigation drawer.
Bootcamp
API calls as actions vs data (and the planning checklist)
When to use Bubble API calls as actions versus data sources, a one-to-many auth pattern, and the planning checklist that keeps integrations from failing silently.