Skip to content
Back to the Bootcamp
The Momentum Bubble Bootcamp, free. Part 2 of the paid course that trained hundreds of Bubble developers — released free, because the thinking it teaches is exactly what great AI-assisted development is built on.

Bootcamp

Build testimonials and a working contact form in Bubble

Build testimonials and a working contact form in Bubble

Photo by Brands&People on Unsplash

There’s a moment in every Bubble build where a page stops being a drawing and becomes an application: the first time an element is filled from the database, and the first time a button writes something back. This tutorial is that moment. Starting from a landing page with header, hero and services in place, we’ll add a testimonials section that reads from the database, a contact form that writes to it (and emails you), and a footer that ties the navigation together, all in mid-fidelity greyscale so the mechanics stay in focus.

A workflow is Bubble’s unit of behaviour: an event (“when Button Send Message is clicked”) followed by a series of actions (create a database record, reset the inputs, show a notification). Everything interactive in a Bubble app is an element wired to a workflow.

Set up the section containers

The page is one big column, each section pushing the next down. Two container habits carry the whole build: outer section groups with max width infinite (backgrounds stretch to the screen edge), padding of 156px left and right and 56px top and bottom, and fit height to content; and inner groups, each named Group Container, with a max width of 1128px (1440 minus 156 each side) and zero padding, since the outer group provides it. Use the minimum number of groups possible and define settings at the highest point in the hierarchy so they flow down.

To scaffold the new sections, right-click Group ServicesCopy, right-click indexPaste (which lands the copy at the right level of the hierarchy), delete everything inside its container, then copy it twice more and rename the three groups Group Testimonials, Group Contact and Group Footer.

Build the testimonials layout

Every section is just boxes: rows and columns. For testimonials, the container is a column holding a heading block, a white quote card, and a row of navigation dots.

  1. Drop a text element in for the heading (“Don’t just take our word for it”). Remove the minimum height and min/max width and tick fit height to content and fit width to content: we’re eliminating settings and letting content define size. Set the font (Inter 600, 32) and centre it with the Layout tab’s horizontal alignment, which centres the box inside its parent (Appearance-tab centring only centres text inside its own box).
  2. Add the subheading the same way. The design has 16px between heading and subheading but 32px between larger blocks, so group the two texts into a column container with a gap of 16 and set the section container’s gap to 32. Spacing gaps instead of unique paddings on every element pay off when the page goes responsive.
  3. Add the quote card, Group Testimonial (singular): a column, not fit-width-to-content (the card should span the container, not hug its contents), centred, white background, roundness 16 (Bubble’s word for corner radius), padding 64 top and bottom, 48 left and right. Padding is space inside the box; margins are outside.
  4. Inside the card add the quote text (Inter 500, 36, centred), then the avatar: export the 48 by 48 placeholder from Figma as a 2x PNG and load it as a static image, fixed at 48 by 48, aspect ratio locked. Add the name (Inter 700, 18) and role (Open Sans 400, 16).
  5. Impose the spacing: group avatar, name and role into a column (“Group Author”) with a gap of 16, then group name and role into their own column with a gap of 4. Every number here (4, 8, 16, 24, 32, 48, 64) is a multiple of four: that ratio discipline is your design system at work.
  6. For the stars, install the Material Icons plugin, add a star at 20 by 20 and copy it four times. Group all five into a row (“Group Stars”), fit width to content, centred, gap 8. Then group the author block and stars into a column (“Group Profile”) with a gap of 24.
  7. The dots are shapes: 8 by 8 fixed with a radius of 4 (radius applies per corner, so 4 makes a circle). Duplicate to five, group into a row with a gap of 8, then wrap that row in a group with a minimum height of 40 and centre the dots vertically to reproduce the design’s 16px above and below. Check in the element tree that the dots sit inside Group Container, not beside it.

Finally, rename everything (Group Name Role, Group Testimonial Nav, Circle 1 through Circle 5): fussy now, a constant saving later.

Make the testimonials dynamic

Five quotes need to rotate through one card, so the data belongs in the database.

  1. Go to Data → Data types and create a type called Testimonial with four fields: quote (text), profile pic (image), name (text), role (text). A data type is essentially a spreadsheet: check App data and you’ll see a table with your four columns plus Bubble’s built-ins.
  2. Create a custom state to track which testimonial is showing: select the index page, open the element inspector’s “i” button, and add a state called display number (number), defaulting to 1.
  3. Select Group Testimonial (the white card), set its Type of content to Testimonial and its Data source to Do a search for Testimonials:item # index's display number. The search returns the list; :item # picks the one matching the state.
  4. Replace static content with dynamic data. Delete the quote text and use Insert dynamic dataParent group's Testimonial's Quote; the canvas placeholder lets you paste the old static text so the editor stays readable. For the image, avoid chaining parent's parent's parent: reference the group directly with Group Testimonial's Testimonial's Profile Pic, and do the same for name and role.
  5. Preview, and enjoy a card full of blanks: the table is empty. Go to App data → New entry and create five testimonials. If your round avatar renders square, give the image a roundness of 24 (half its 48px size).
  6. Wire the dots. On Circle 1, Add workflow: when Circle 1 is clicked → Set stateindex's display number = 1. Copy and paste the workflow for circles 2 to 5, changing only the value.
  7. Show the active dot. On each circle, add a condition: when index's display number is 1 → background colour dark; default light. Use Copy condition / Paste condition to replicate, changing the number each time. (The dots are small targets; a refinement later is wrapping each in a larger clickable group.)

One note on Data → Privacy: testimonials are for a public website, so the default publicly-visible setting is fine here. Hold that thought.

Build the contact form layout

The contact section reuses the same box thinking: a heading block (copy the one from testimonials and restyle: heading Inter 600, 44; subheading Inter 700, 18), then a form container, then a row of contact details.

  1. Add Group Form Container: column layout, centred, max width 664, spacing gap 24. Setting the width once on the top-level container beats setting it on every field.
  2. Add an Input named Input Email: fixed height 40, width settings removed so the parent guides it. Add a caption text (“Email”) above it and group the pair into a column (“Group Email Field”) with a 2px gap.
  3. Repeat for the topic with a Dropdown (height 40): text inputs suit free text, dropdowns suit categorical data. Group it with its caption as Group Topic Field, then group both field groups into a row with a gap of 24 so email and topic sit side by side. If a paste lands in the wrong layer, right-click the element and Reveal in element tree to fix it.
  4. Add a Multiline Input for the message with its caption, grouped as Group Message, spanning the full width. Reorder siblings with the Layout tab’s previous / next / make first / make last controls.
  5. Add a Button, “Send message”: fixed height 48, fit width to content, padding 12 top and bottom and 16 left and right, radius 4, centred. Style the section while you’re here: white background on Group Contact, the light grey card on its container, paddings of 32 on the section and 40 on the card.
  6. For the contact details, build a row (“Group Contact Details”, max width 856, centred) holding three icon-plus-text groups (address, phone, email), each a 24 by 24 Material Icon and a text with a 12px gap, vertically centred, spread with space between. One catch: space between only truly centres the middle item if the outer items are equal widths, so set a minimum width of 260 on the two outer groups and left-align the first.

Preview it, ignore the unstyled fonts (styles get systematised in the next module), and make it work.

Wire the form: validation, option sets and the workflow

First, validation. On Input Email, set the content format to Email; the input now refuses anything without an at-sign and domain, and an invalid input blocks the button from submitting. Other formats (password, integer, decimal, date) suit other fields; the multiline input stays unrestricted because a message really is free text. Also useful: this input should not be empty makes a field mandatory, and prevent enter key from submitting stops the enter key firing early, since Bubble treats all inputs on a page as one form.

Second, the option set for topics. Rather than typing static choices into the dropdown, go to Data → Option sets, create Contact Us Topic, and add your options. On the dropdown, set choices style to Dynamic choices, type of choices to Contact Us Topic, choices source to All Contact Us Topics, and option caption to Current option's Display. The list now lives in one place, reusable anywhere in the app.

Third, somewhere to store submissions: a data type Contact Us Submission with fields email (text; you can validate an email but it’s stored as text), topic (the option set) and message (text).

Now the workflow. Select the Send Message button and click Add workflow:

  1. Data → Create a new thing → Contact Us Submission. Creating a thing means adding a row to that table. Set the fields: email = Input Email's value, message = Multiline Input Message's value, topic = Dropdown Topic's value. Rename the dropdown “Dropdown Topic” first; searchable names make expressions dramatically faster to build. And watch the types: the topic field expects a Contact Us Topic, so supply the dropdown’s value, not its display; a red expression means a type mismatch.
  2. Test it: press the button, then check App data → Contact Us Submissions. The row is there, newest first, but the user saw nothing happen, which is poor usability. So:
  3. Add Reset relevant inputs as the next action, clearing the form on submit.
  4. Add a toast: install the BEP Toast Message plugin and use its toast alert action with “We have received your message”. The plugin saves building the fade-in, pause and fade-out yourself.
  5. Help the admin as well as the visitor: add a Send email action to your own address, subject “New contact us submission”, body composed of Input Email's value, Dropdown Topic's value's Display and Multiline Input Message's value.
  6. Test the whole chain, using plus addressing (you+1@yourdomain.com, you+2@...) to submit repeatedly with “different” emails. If a value doesn’t arrive, walk backwards: was the field filled, did the workflow map it, does the row show it?

One more thing, and it matters: privacy. The new data type is publicly visible by default, meaning a clever web developer could find and expose the table’s contents. Fine for blog posts or product listings; not fine for contact submissions. Go to Data → Privacy and define a new rule on Contact Us Submission (we name it “no access!”). Give it a condition that can never be true, Current User is not Current User, and untick every permission on it and on “Everybody else”: view all fields, find in searches, view attached files. The data still reaches you by email and remains visible in the editor’s App Data tab, but nobody using the app can reach it.

The footer is a container with three stacked pieces.

  1. Group Top: a row with space between, holding the logo (copy the header’s; it’s the same 168 by 27 image) and the nav links (copy Group Nav from the header, gap 32, Inter 700, 16). Remove fixed widths that break the row, fit to content, and vertically centre the nav against the logo.
  2. Shape Divider: a shape with no background and a 1px top border in the divider grey (#CED0D5), full width, no minimum height. Set the container’s gap to 32.
  3. Group Bottom: another space between row with the social icons on the left and the copyright text (Inter 400, 14) on the right. For the socials, use Link elements displayed as icons rather than plugin icons with click workflows: a real HTML link is better for SEO. Set each to the external URL, opening in a new tab so visitors aren’t led off your site, sized 24 by 24 with the icon font around 20 to fit, grouped in a row with a 12px gap. Delete any leftover wrapper groups; spares just cause confusion.

Then wire the navigation. On each footer nav item (and the header’s, and the hero’s buttons), Add workflowElement actionsScroll to the matching section: About scrolls to Group Services, Testimonials to Group Testimonials, Contact to Group Contact. The same action ten times, fast precisely because everything is named.

Two refinements complete the page. First, an entrance animation: hide Group Services by default, then add a Do when condition is true event with Current page scrolling position > 300 and an Animate action on Group Services, so it fades in on scroll (make sure the element you hide is the element you animate). Second, a sticky header: right-click Group Header, Replace element typeFloating Group, floating relative to top, background opacity 100. A floating group sits on top of the page rather than taking up room, so add 64px back to the hero’s top padding and set an offset of -64 on every scroll-to action so sections don’t land under the header. Finally, tidy the Workflows tab into folders (Navigation, Testimonials, Contact Us, Animation) so you can audit each behaviour and count that all ten scroll actions are present.

The lesson hiding in the repetition: hooking things up in Bubble is quick. The slow, valuable part is knowing what to build, which is why design and definition always precede the wiring.

This skill in the AI era

This tutorial is really three transferable ideas wearing Bubble clothing: separate structure from content, keep interface state explicit, and treat every write as an event with consequences (validate, store, confirm, notify, protect). Those ideas are exactly what we look for when we rescue and migrate Bubble apps: apps built with this discipline translate cleanly to code, while apps without it need archaeology first. When Claude Code builds the modern equivalent, the machine writes the components but you still specify the validation rules, the option sets and the privacy posture. The move from Bubble to AI-assisted coding changed the editor, not the thinking.

If this sparked something, let's talk.

No pitch, no pressure — just a conversation about what you're working on.

Let's talk
Share: