How to make each template work across desktop, tablet, and mobile using Bubble's responsive engine.
Bubble uses a flexbox-based layout system with rows and columns. Key concepts:
If your Bubble app still uses the legacy fixed-width engine, you'll need to migrate to the responsive engine first. In the Bubble editor, go to Settings → General → check "Make this application responsive."
Bubble doesn't have explicit breakpoints like CSS media queries. Instead, you achieve responsive behavior through:
1. Flexible widths (min/max instead of fixed)
2. Row wrapping (children flow to next line)
3. Conditional visibility (hide elements at narrow widths)
| Device | Viewport Width | Layout Approach |
|---|---|---|
| Desktop | 1024px+ | Full layout — sidebar + main content, multi-column grids |
| Tablet | 768px – 1023px | Collapsed sidebar, 2-column grids |
| Mobile | < 768px | Single column, hamburger menu, stacked layout |
Desktop (1024px+):
Tablet (768px):
Mobile (<768px):
Implementation:
Desktop: 3-column listing grid, sidebar filters visible
Tablet: 2-column grid, filters collapse into a "Filters" button that opens a popup
Mobile: 1-column listing cards, search bar at top, filters in a bottom sheet
Key responsive elements:
Desktop: Calendar shows full week view, sidebar with service list
Tablet: Calendar shows 3-day view, service list above calendar
Mobile: Calendar shows day view only, available slots in a scrollable list
Key responsive elements:
Desktop: Map + list split view (60% list, 40% map), sidebar filters
Tablet: List view with map toggle button (not split)
Mobile: List view only, map accessible via "View Map" button, search bar sticky at top
Key responsive elements:
Desktop: Three-column layout (sidebar nav, main content, trending/leaderboard sidebar)
Tablet: Two columns (nav sidebar collapsed, main content + trending below)
Mobile: Single column, bottom navigation bar, trending hidden
Key responsive elements:
Works for: SaaS Dashboard, Booking System
1. Create the sidebar as a group with min-width 0 and max-width 240
2. Add a custom state sidebar_open (yes/no, default: yes)
3. When sidebar_open = yes → width = 240, show labels
4. When sidebar_open = no → width = 60, show icons only
5. On page load: If page width < 768, set sidebar_open = no
6. Add a hamburger icon that toggles the state
Convert horizontal rows to vertical columns on narrow screens:
1. Use a Row container with wrapping enabled
2. Set each child's min-width to approximately 50% of mobile viewport (e.g., 300px)
3. On desktop, children sit side by side
4. On mobile, wrapping causes them to stack vertically
This is the single most useful responsive pattern in Bubble.
Desktop: Horizontal nav bar or vertical sidebar with text labels
Mobile: Bottom tab bar (for apps) or hamburger menu (for content sites)
Implementation:
1. Create two navigation elements: desktop_nav and mobile_nav
2. desktop_nav: visible when page width >= 768
3. mobile_nav: visible when page width < 768
4. mobile_nav is a floating group pinned to the bottom with 4–5 icon tabs
Tables don't work well on mobile. Three alternatives:
1. Card layout: Each row becomes a card with fields stacked vertically
2. Horizontal scroll: Wrap the table in a group with horizontal scrolling
3. Priority columns: Show only essential columns on mobile, full table on desktop
Card layout is best for most cases:
Desktop: Repeating group layout = Full list (table rows)
Mobile: Repeating group layout = Extended (card-style cells)
Use Bubble's responsive text settings:
Set these in your app's Styles tab for consistency.
1. Bubble's responsive viewer: In the editor, click "Responsive" tab to drag the viewport width
2. Chrome DevTools: F12 → toggle device toolbar → select device presets
3. Real devices: Always test on at least one actual phone — emulators miss touch target size issues
"My sidebar overlaps the content on mobile"
Set the sidebar's min-width to 0 and use conditional width. Make sure the main content group doesn't have a fixed left margin — use the sidebar as a flex sibling instead.
"Text is too small on mobile"
Check your font sizes. Body text should be at least 14px on mobile. Don't rely on users pinch-zooming.
"Buttons are too small to tap"
Minimum touch target size is 44x44 pixels (Apple's guideline). Make sure buttons and clickable elements meet this on mobile.
"My repeating group is just one column on desktop"
Check that the repeating group's layout is set to Grid or Extended, not Full List. For Grid, verify the number of columns setting.
*Responsive design isn't about making things fit — it's about making things work on every device.*
Common patterns that apply across all five app templates. Use these as building blocks — they're the same regardless of whether you're building a SaaS dashboard, marketplace, booking system, directory, or community platform.
Every app needs this three-step flow:
1. Sign up: Use Bubble's built-in "Sign the user up" action with email + password
2. Set profile fields: Immediately after signup, use "Make changes to Current User" to set display_name, role, and any default values
3. Redirect: Navigate to the main page (dashboard, homepage, etc.)
When a non-logged-in user tries to access a protected page:
1. Set a custom state or URL parameter with the intended destination: /dashboard?redirect=/settings
2. On the login page, after successful login, check for the redirect parameter
3. If it exists, navigate there. Otherwise, navigate to the default page
Bubble has a built-in password reset flow:
1. "Send password reset email" action — sends a link to the user's email
2. The link goes to a page you designate (e.g., /reset-password)
3. On that page, use "Reset password" action with the token from the URL
On every protected page, add a page-load workflow:
When: Page is loaded
Condition: Current User's role is not [required role]
Action: Navigate to /unauthorized (or redirect to login)
For elements that should only appear for certain roles:
Element conditional:
When: Current User's role is Admin
This element is visible: checked
(Default: not visible)
On every workflow that modifies data, add a condition:
Workflow condition: Current User's role is Admin or Manager
This is your second line of defense after privacy rules. Even if someone manipulates the UI to reveal a hidden button, the workflow won't execute without the correct role.
Use custom states on the page to store filter values. When any filter changes, update the state. Your repeating group's data source references the states.
Setup:
1. Create custom states on the page element: filter_category (text), filter_min_price (number), filter_sort (text)
2. When a dropdown value changes → Set state filter_category to dropdown's value
3. Repeating group data source: Search for Things where category = page's filter_category
Why states instead of directly referencing inputs? States persist across tab switches, survive element re-rendering, and make your data source expressions cleaner.
For search inputs, you don't want to trigger a search on every keystroke. Two approaches:
1. Search on Enter: Add a workflow "When Input's value is changed" with a "Pause before next action: 500ms" step. If the value changes again during the pause, the workflow restarts, effectively debouncing.
2. Search Button: Only search when the user clicks a button or presses Enter. Simpler and more predictable for complex searches.
Always handle the case where search returns no results:
Group "empty_state":
Visible when: Repeating Group's list of Things:count = 0
Content: "No results found. Try adjusting your filters."
Bubble's built-in repeating group pagination works for most cases:
1. Set "Fixed number of cells" on the repeating group (e.g., 12)
2. Add "Show previous" and "Show next" links to the repeating group
3. Display current page: Use the :current page number property
For infinite-scroll-style UX:
1. Create a custom state items_to_show (number), default 12
2. Repeating group data source: Search for Things:items up to items_to_show
3. "Load More" button workflow: Set state items_to_show to items_to_show + 12
4. Hide the button when items_to_show >= total items count
Used in: SaaS Dashboard, Marketplace, Community Platform.
Data type: Notification (recipient, type, message, link_url, is_read, triggered_by, created_date)
Creating notifications: Add a "Create Notification" step to relevant workflows (new message, new review, status change, etc.).
Displaying the bell badge:
Text element in nav bar:
Content: Search for Notifications where recipient=Current User AND is_read=no:count
Visible when: content > 0
Style: red circle badge
Notifications page: Repeating group sorted by created_date descending. Each row has icon, message, time ago, and link. Clicking navigates to link_url and marks as read.
Use Bubble's "Send email" action for important events. Always respect the user's notification_email preference:
Workflow condition: Target User's notification_email = yes
1. Use Bubble's Picture Uploader element
2. Below it, add an Image element with data source = "Picture Uploader's value"
3. This shows a preview of the uploaded image before the form is submitted
1. Use a MultiFile Uploader (or multiple Picture Uploaders)
2. Store images as list:image on the data type
3. Display in a horizontal repeating group with fixed cell width
4. First image in the list is the "cover" — display it larger elsewhere
For user avatars, handle the case where no image is uploaded:
Image element:
Dynamic source: Current User's avatar
Conditional: When Current User's avatar is empty → show static default avatar image
Bubble's repeating groups auto-refresh when their data source changes. For near-real-time updates (like chat messages), this works out of the box — no manual polling needed.
For true real-time (sub-second), consider:
For actions like upvoting, show the change immediately without waiting for the server:
1. User clicks upvote → immediately update a client-side state to show +1
2. In parallel, the workflow creates the Vote record and updates the count on the server
3. When the repeating group refreshes, the server count overwrites the optimistic count
This feels faster to the user. If the server action fails (rare), the count will correct itself on refresh.
Before creating or modifying records, validate inputs in the workflow condition:
Workflow condition:
Input title is not empty
AND Input title:length > 3
AND Input email contains "@"
AND Input price > 0
If validation fails, the workflow doesn't run. Show error messages using conditional alerts:
Group "error_title":
Visible when: Button was clicked AND Input title is empty
Content: "Title is required"
Style: red text
Before creating a record with a field that should be unique (like a slug or username):
Workflow step 1: Search for Things where slug = generated_slug
Condition for step 2: Search result:count = 0
Step 2: Create the thing
Error: If count > 0, show "This name is already taken"
Instead of permanently deleting records, mark them as deleted:
1. Add a is_deleted (yes/no) field to the data type
2. "Delete" workflow: Set is_deleted = yes instead of using "Delete thing"
3. All searches include constraint: is_deleted = no
4. Admin can see deleted items and permanently remove them or restore them
5. Scheduled backend workflow: Permanently delete records where is_deleted = yes AND deleted_date was more than 30 days ago
This prevents accidental data loss and supports "undo" functionality.
After any action that might fail (payment, email, external API call):
Step N: [Action that might fail]
Step N+1 (only when result of N is empty/error):
Show alert: "Something went wrong. Please try again."
OR
Navigate to /error page
For elements that depend on data that might not exist:
Group "data_section":
Visible when: This Thing is not empty
Group "empty_section":
Visible when: This Thing is empty
Content: "No data available yet. [Call to action]"
*These patterns appear in every no-code app you'll ever build. Master them once, apply them everywhere.*