Bootcamp
Design systems in Bubble: styles, buttons and icons
Photo by Hal Gatewood on Unsplash
There’s a moment in every Bubble project where you change a font on one button and realise you now have to change it on forty more. That moment is the argument for design systems, and Bubble’s styles tab is where you encode one, so that consistency is enforced by the tool rather than by your memory.
This tutorial covers the full pass we do in Module 3: the microinteraction checklist that defines what your components need to do, then the styles machinery that makes it all maintainable — style variables, font styles, button states with proper transitions, nav links, icon groups, and images.
A design system is a set of components, rules, style guides and documentation used to build a coherent, on-brand interface. In Bubble, you implement it through the styles tab: define each rule once, centrally, and apply it everywhere. Never configure the same look element by element.
First, the checklist: microinteractions
Microinteractions are the small behaviours that keep users in the loop about what’s happening. They take time to build, so they’re tempting to skip on a deadline, but they’re what makes a product feel mature rather than merely functional. The checklist we hold our own agency work to:
- Alerts and spinners. When a button kicks off a running process, show one.
- Buttons go inactive when pressed. If pressing again would re-trigger a backend process, disable the button.
- Error handling on inputs. Validation plus an error caption under the field guiding the user to the correct input; labels above fields help prompt them in the first place.
- Confirm before destruction. A pop-up before deleting data, and before letting a user leave a form with unsaved changes. Every modal needs a title, a prompt, a primary and secondary action, and a cross icon to escape.
- Empty states. An empty list should say so (“no support requests; widen your search”) instead of looking broken. An empty field should show “N/A” or “not supplied” in a lighter font for the same reason.
- Hover and tap states. Hover for mouse users, tap states as the mobile equivalent: the signal that you’re about to act, or just did.
- Escape hatches. Breadcrumbs, back buttons, a cancel on every modal. Users must always have a way back.
- Lists get search, sort, filter and pagination. Users expect them without knowing they expect them, right up until one is missing, when they report it as a bug.
Everything below is about encoding these behaviours and your visual rules into styles.
The styles tab, and the one rule that matters
Bubble ships with default styles for most element types; you’ll replace the ones you use with your own. A style has three tabs mirroring an element’s properties (appearance, layout, conditionals) but deliberately limited to what’s generalisable. You can’t express “when this input’s value is under 100, red border” at the style level; that belongs on the element. But fonts, colours, backgrounds, borders, radius, padding, hover states, transitions, and conditionals like “when screen width is below 768 pixels, set left and right padding to 20” can all live on the style, applied once for the whole app.
The workflow is: create a style, apply it to elements, and only rarely detach (which leaves you with a one-off) or override (change one property, say centring this instance, while everything else keeps following the style). You can also create a style from an element you’ve already formatted, via the option at the bottom of its style picker. Later in the course we name the underlying principle, don’t repeat yourself (DRY), but you can see the payoff immediately: three groups each carrying identical hand-set formatting is three times the data and three places to make mistakes, while three groups pointing at one style is one change, everywhere.
Naming matters too. Styles called Heading One, Heading Two, Primary Button give any developer who opens the app, including future you, a convention they already understand. And don’t stop at text: container settings repeat constantly, so a style called Main for your standard section container earns its keep fast.
Style variables: the app font and colours
Before building individual styles, set the style variables, the values styles refer to.
- In the styles tab, switch to style variables and set the app font (ours is Inter; nearly everything offered is a Google font, which Bubble supports natively). Now every style can point at
App fontinstead of naming Inter directly. Change it once, rebrand everywhere. - Below fonts are colour variables, up to 32 of them. Bubble provides eight defaults you can’t delete (primary, primary contrast, text, surface, background, destructive, success, alert); the rest are yours. Create the weights your design actually uses: for our landing page that’s
Primary,Primary 200, plus aGrey 300and aShade Off Whiteas we need them. Copy the hex codes straight from the Figma colours tab, and use the description field (“main brand colour”) as documentation.
One honest limitation to note: the Figma design puts gradients on some text, and gradient text simply isn’t possible in Bubble without custom CSS. Flat primary it is.
Font styles: building Heading One
With variables in place, build the text styles. For the hero heading:
- Create a style named
Heading One. Delete Bubble’s stale defaults for text while you’re there; filter the list by element type Text to find them. - Set the HTML element to h1. This matters for SEO.
- Font: App font, weight 700, size 56.
- Line spacing: the design says line height 68 on size 56, and Bubble wants the ratio, so 68 ÷ 56 ≈ 1.21.
- Colour: point it at the Primary colour variable, not a raw hex. Now if the brand colour shifts, every style referencing the variable follows.
- Leave backgrounds, borders and shadows off, leave layout to the containers, and delete any conditional you don’t need.
Apply Heading One to the hero text and you’ve done it the right way: element points to style, style points to variables.
Buttons: states, transitions, and the box model trap
Buttons are where the design system earns its keep, because they carry states.
Primary button. The text is App font, weight 600, size 16, line spacing 24 ÷ 16 = 1.5, letter spacing 2 per cent of 16 = 0.32 pixels. Padding is 12 top and bottom, 16 left and right (hold Option/Alt over the Figma button to read these), corner roundness 4, everything in multiples of four. Background: the Primary variable. No shadow.
Then the states, all defined in the style:
- Hover: a conditional (
when this Button is hovered) sets the background colour to Primary 200. - Transition: at the bottom of the appearance tab, define a transition on background style, duration 200 milliseconds, ease. Bubble’s transition list doesn’t offer background colour exactly; background style is the one, and it makes the hover fade in and out with a gentle feel instead of snapping. (This is also where the gradient limitation bites: Bubble can’t transition a gradient background, which is why we kept the buttons flat.)
- Disabled: a conditional on
when this Button isn't clickablesets the background to Grey 300, with no transition needed into a disabled state. Test it by ticking “this element isn’t clickable” on a button, or drive it from a conditional.
Secondary button. Same typography. Background Shade Off White, text Primary, plus a 1-pixel solid border in the primary colour, roundness 4. On hover, reverse the colours: background Primary 200, text and border to the off-white.
Preview both side by side and you’ll spot something subtle: the secondary button is slightly bigger. Think back to the CSS box model. An element’s size is content plus padding plus border, and this one carries an extra pixel of border all round, so it’s two pixels taller and wider. The fix: take one pixel off every padding side of the secondary style to compensate. The same thinking applies on hover; keep a border occupying its space in every state so the button never changes size under the cursor.
Nav links: the tertiary tier
The header links are conceptually tertiary buttons, a third priority tier below primary and secondary actions, even though we build them as text (their padding and underline behaviour don’t fit the button element). In their style:
- Appearance: define each border independently, then give the text a default 4-pixel solid bottom border in the background colour. Invisible, but holding its space so nothing jumps on hover.
- Conditional:
when this Text is hovered, bottom border colour becomes Primary. - Add transitions on the bottom border colour and the font colour so both fade.
- For the text itself on hover, a lighter shade looked odd against the background in testing; a darker weight, primary 600 or 700, gives a subtler and better result. Expect some trial and error here. Preview, adjust, preview again is the honest workflow.
Icons in circles
The feature cards need icons in gradient circles, replacing the shape placeholders from Module 1.
- Install the Material Icons plugin (Google’s icon set) from the plugins tab.
- A shape can’t be converted to a group, so recreate the placeholder as a Group: fixed 56 × 56, roundness 28 (half the width makes the circle).
- Background: a custom gradient. Bubble supports three stops, and finding the angle is pure trial and error; ours landed at 135 degrees to put the light stop top-left and the dark stop at the bottom. You won’t perfectly match a four-stop Figma gradient, so settle for close.
- Set the group’s layout to align to parent, drop a Material Icon inside at a fixed 24 × 24, place it in the centre cell, and colour it with the off-white variable.
- Be a good developer: save the group’s configuration as a style,
Group Icon Circle, then copy and paste for the other cards, swapping each icon. The pasted copies position themselves with layout’s previous control.
Images, overlays and compression
The final pass brings the page into high fidelity, and three techniques matter.
Groups beat image elements for backgrounds. A group with a background image gives you options an image element lacks: centre the image, make it as wide as the parent, crop, repeat, and layer a background colour. Export assets from Figma at the frame level (not the loose vector) as 2x PNG or SVG, and expect to hunt through Figma’s layer tree to find the right one.
Overlays fix contrast. Our services section sits on a photographic background where white text struggled. The designer’s trick: a group inside the image group, Group Overlay, carrying the parent’s padding and a black background at 10 per cent opacity. The photo darkens slightly, the contrast ratio improves, the text reads.
Compress everything with Imgix. A 4x export of the hero image weighed three megabytes. On the web you want well under one, ideally under 500 kilobytes, because every extra byte is download time on someone’s phone. Store the image dynamically (we used an option set with an image attribute), reference it with dynamic data, then append :processed with Imgix to the expression and set quality: 25, as JPEG, worked fine. Verify in your browser’s inspect tool under sources. Ours dropped from three megabytes to 90 kilobytes, three or four per cent of the original, with no visible loss at page scale.
Finish the sweep: the floating header gets its drop shadow (outset, 2 / 7 / 12, #222222 at 8 per cent), the footer gets the primary background with a subtle 1-pixel primary-200 top border, white footer links get a grey-200 hover with the standard 200-millisecond transition, and every colour on the page points at a variable. When you preview, remember the debug bar pushes the page up; turn it off before judging spacing.
This skill in the AI era
A design system is a set of decisions made once and enforced by tooling, and that idea has only grown more valuable now that AI writes the components. When we direct Claude Code, the style tokens, state rules and microinteraction checklist above become the brief, and the model applies them with perfect consistency across hundreds of files; the builder’s job is owning the system, just as it was in Bubble. It’s also the first thing we look for in an inherited app — style-less, hand-formatted pages are the signature of a build that needs help, and untangling them is bread and butter in our Bubble rescue and migration work.
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.