Dark Mode without White Flash and with a Near-Instantaneous Toggle

Hello, Bubblers!

I have playing around with light/ dark mode functionality in my Bubble apps for a while now, and it appears the community consensus for the best way to achieve this is to set a condition on every style or element to detect when a field on a user (often called “dark-mode?” with a yes/no value) is set to yes, and then change the color based on this value.

However, this process requires significant diligence in ensuring every single element or style in your app has the requisite condition. Moreover, there is a constant “white flash” every or almost every time the page is loaded when “dark-mode” is set to “yes”. Add to this the decreased performance in your app due to so many attached conditions (try to spam click your theme toggle in an app that implements this method – it is laaagggyyyy), and you can see why a more efficient method is desired.

I believe I have come up with such a method, and will attempt as best I can to describe it below. Its main benefits include the nonoccurrence of the white flash when the theme is set to dark, and there is a near-instantaneous toggle of light and dark themes.

The Old Way

ScreenRecord-UD

The New Way

ScreenRecord-MC

First, I would like to point out a couple of prerequisites you will need to have before implementing this method:

  • It requires you be on a paid Bubble plan, as we will use the “Page HTML Header” and “Script/Meta tags in Header”, and both of these require you being on a paid Bubble plan (or free trial).
  • You will have to know ALL of your corresponding color variables. This means that in addition to having designed your app with the default/ light colors, you must know which colors you will be using in dark mode that will replace these default/ light colors.

Let’s begin…

Step 1 – Get Your Default Color Variables

We start by first obtaining all of our default color variables. Of course, we have those listed under the Style Variables tab in the Bubble editor, but we need to access the actual CSS variable name for each. This post from havenocode provides a good description about how to retrieve those values.

Essentially, you’ll need to preview your app in a browser, inspect the page and open DevTools. Then you’ll select the main html tag at the top of the code in the Elements panel, and once the main html is highlighted, select the Styles panel to view your default color variables. These variables will be located under either a class of root or b.root within the Styles panel.

Copy and paste these variables into a text editor, and remove the duplicate values (those ending with suffix _rgb). At this point, you should have your default color variables in a list that looks something like this:


  --color_text_default: rgba(0, 0, 0, 1);
  --color_alert_default: rgba(250, 181, 21, 1);
  --color_primary_default: rgba(0, 0, 0, 1);
  --color_success_default: rgba(23, 219, 78, 1);
  --color_surface_default: rgba(255, 255, 255, 1);
  --color_background_default: rgba(230, 230, 230, 1);
  --color_destructive_default: rgba(255, 0, 0, 1);
  --color_primary_contrast_default: rgba(255, 255, 255, 1);
  --color_bTHOj_default: rgba(64, 64, 64, 1);
  --color_bTHOk_default: rgba(230, 230, 230, 1);

I should note that the color values can be hex, rgb, rgba, or hsl, but because Bubble uses rgba, I chose to proceed with the rgba format for consistency.

Step 2 – Assign Corresponding Dark Color Variables to your CSS Variable Names

Right now, we have one color assigned to each CSS color variable (see above). Now, we need to define what colors for our dark theme will replace each default/light color. Basically, if you have a white background for default/ light mode, you would want to have a black (or close to black) background for your dark mode. But we will keep the same CSS variable name, and merely change the color when a particular theme is set.

I recommend a spreadsheet setup to accomplish this:

Step 3 – Styling and Placing Your Dark Mode Color Variables

So we have our dark mode color variables, and we now know which CSS variable name each color is assigned to, both in dark and light modes. Next, we must correctly format these color variables to override our default/ light color scheme, and we will place the styling in the “Page HTML Header” input of every page we want to implement dark mode.

To correctly format our styling, we will start with our list of default color variables in Step 1. Then, we will replace each color (NOT the CSS variable name, but the color) with its corresponding dark color value that we assigned in Step 2 in our spreadsheet. Once we have done this, we’ll append an “!important” declaration after each dark color value – this allows us to override our default/ light colors. Finally, we’ll assign a class to these color variables that attaches to the main html document of our app (the “html[data-theme=‘dark’]” in my example below), and wrap the entire thing in style tags:


<style>
html[data-theme='dark'] {
  --color_text_default: rgba(255, 255, 255, 1) !important;
  --color_alert_default: rgba(250, 181, 21, 1) !important;
  --color_primary_default: rgba(255, 255, 255, 1) !important;
  --color_success_default: rgba(23, 219, 78, 1) !important;
  --color_surface_default: rgba(26, 26, 26, 1) !important;
  --color_background_default: rgba(38, 38, 38, 1) !important;
  --color_destructive_default: rgba(255, 0, 0, 1) !important;
  --color_primary_contrast_default: rgba(0, 0, 0, 1) !important;
  --color_bTHOj_default: rgba(191, 191, 191, 1) !important;
  --color_bTHOk_default: rgba(51, 51, 51, 1) !important;
}
</style>

The reason we assign a class to our main html document is this will load before the body of our page is rendered – preventing the dreaded white flash. Now, for this styling to work correctly, we’ll need to place it in our Page HTML header section of every page. Here is what my implementation looks like from the demo above:

I should also note that because you cannot directly change the color of the body (the background of your page that is BEHIND the Page “background” in the Bubble editor), I define the background color for the body at the bottom of this styling. This also helps prevent the white flash, as our body background color will load before our page is rendered, and the color will be the same as our page background.

Step 4 – Set and Get localStorage on Page Load

Some may or may not know of localStorage. As indicated by W3schools, “the localStorage object stores data with no expiration date. The data is not deleted when the browser is closed, and are available for future sessions.” The localStorage object requires a key (our ‘data-theme’) and a value (for our example, the value will either be ‘light’ or ‘dark’).

This is indeed an incredibly useful tool that allows our light/ dark mode selections to persist and be remembered by our application, even after the browser is closed and reopened on the same device. It is also way less impactful on your app’s resources, in contrast to switching a field on a given User’s data type.

We will use localStorage to remember our User’s dark/ light selection by inserting the following JavaScript in the “Script/ Meta tags in Header” multiline input:


<script>
if (localStorage.getItem('data-theme') === null || localStorage.getItem('data-theme') === 'light') {
    localStorage.setItem('data-theme', 'light');
    document.documentElement.setAttribute('data-theme', 'light');
} else {
    localStorage.setItem('data-theme', 'dark');
    document.documentElement.setAttribute('data-theme', 'dark');
}
</script>

This script is telling us that if a User’s “data-theme” in localStorage is “null” or “light”, to set the localStorage “data-theme” to “light”, and assign our default/ light class to the main html document. Again, because this default/ light class contains our default colors in Bubble, the page will be rendered with our default/ light colors.

However, the script continues on and tells us that if a User’s “data-theme” in localStorage is not “null” or “light”, but instead is “dark”, then to set our “data-theme” in localStorage to “dark” and assign our dark class (the colors under our “html[data-theme=‘dark’]” located in Step 3).

I understand this may be a tad confusing. However, so long as you maintain the same keys and values for your dark and light mode classes and localStorage, the script will function as anticipated.

Step 5 – Install Toolbox Plugin and Configure Front-End Toggling for the User

At this point, we have our defined classes, and a script that runs on page load that sets our “data-theme” class on the main html document based on a user’s localStorage “data-theme” value. But now we need to enable a method by which the User can toggle this value to use a different mode if they so choose. For this, we’ll need to install the Toolbox plugin, which allows us to use JavaScript and retrieve values from JavaScript back to Bubble.

Once Toolbox is installed, place an Expression element and JavaScript to Bubble element on the page. It is important that regardless of their overall placement on the page, the Expression element must be placed AFTER the JavaScript to Bubble element.

Add a suffix to the JavaScript to Bubble element so we can reference the value in JavaScript, check “Publish Value”, and ensure the output value’s selected format is “text”.

Then input the following into the Expression element and set its “Result Type” to “text” as well:

bubble_fn_theme(localStorage.getItem('data-theme'));

Note that your suffix (the text that comes after “bubble_fn_”) can be anything so long as it matches your suffix input in the JavaScript to Bubble element above, as it is a direct reference to our JavaScript to Bubble element.

Also note that we are retrieving the value of our “data-theme” from localStorage (“light” or “dark” for our example) and passing that value to our JavaScript to Bubble element on page load, so we can track what a User’s localStorage is and toggle that value within Bubble. Don’t worry that the localStorage won’t be set – our script in Step 4 assigns a value of “light” if the localStorage “data-theme” is empty.

Once we have our JavaScript to Bubble element and Expression element setup, we’ll place a button on our page within the Bubble editor. This will be our “toggle” button for the User. Select “Start workflow” within the button’s Appearance tab, and this will take us to our Workflow tab in the editor with an unfinished workflow of “When this button is clicked…”

Now, we only want our default/ light mode to be toggled on when a User is currently using dark mode, and vice versa. So we need to insert a condition on the “When this button is clicked” workflow to check if a User’s current theme is light. To do this, we’ll use our JavaScript to Bubble element’s published value, which contains our current theme as passed by our localStorage’s “data-theme” on page load. The condition on the workflow should now look like “When this Button is clicked and JavaScript to Bubble’s value is light”.

Next, we’ll add a step to this workflow by clicking on the “Click here to add action” immediately under the workflow, where we’ll see the “Plugins” option in the sidebar, hover over that option, and select “Run javascript”.

We will insert the following code into the Run JavaScript step:


localStorage.setItem("data-theme", "dark");
document.documentElement.setAttribute('data-theme', 'dark');
bubble_fn_theme(localStorage.getItem('data-theme'));

The script is setting our localStorage “data-theme” to “dark”, setting the “data-theme” class of our main html document to “dark”, and sending the “data-theme” value from localStorage to our JavaScript to Bubble element so we can reference it again, principally if the User would like to switch back to the default/ light mode.

Finally, we will duplicate this workflow and swap out our “dark” and “light” values, so that when the User clicks the button again and the current theme is “dark”, the localStorage, JavaScript to Bubble element, and the class of our main html document will all revert back to “light”. Essentially here, we are flipping all “light” and “dark” values in the duplicate workflow.


localStorage.setItem("data-theme", "light");
document.documentElement.setAttribute('data-theme', 'light');
bubble_fn_theme(localStorage.getItem('data-theme'));

So now we have a single button that will flip the current User’s theme from one to the other, depending on what the current theme is.

That’s it!

I hope this was helpful to those desiring a more compact, efficient, and streamlined approach to implementing dark mode into your apps with Bubble.

Again, I understand the process may be confusing and complex, and I attempted to explain it as well as a non-programmer could. If there is anything that needs clarification or requires further documentation, please don’t hesitate to reach out, and I will do my best to resolve any discrepancies. Also, any and all feedback is welcome! Thanks for reading.

5 Likes

Hey @justinbc4 amazing tutorial!! I’m implementing this on one of my apps for testing purposes, but, I’m getting a weird behavior.

When ever I make an update in the editor, and load reload the app, only the background loads the color correctly, and when I toggle the dark/light mode, only the background changes. In order to achieve the desired behavior I must change the mode and then reload…

Is there anything I might be missing that might cause this issue?

Thank you for your kind words and for reaching out! I’m fairly booked this week with my firm (I’m an attorney), but I’ll take a look at the end of this week/ this weekend, see what could be causing this behavior, and hopefully provide a fix by then. Thanks again for your interest!

Ok, so without access to your app, it is a bit difficult to determine exactly what may be causing the issue. However, I believe I have been able to replicate the issue in my own app.

Regarding your app not changing color until you reload the page/ make an update in the editor and then publish that change: Ensure you have appended !important after each dark mode corresponding color variable in the “Page HTML Header” (see my example in the original post above). If the !important declaration is not made after each dark mode color variable, they won’t be able to override the default color variables until the page is reloaded.

Regarding your app’s elements not changing colors except for the background of your page: Ensure you are using a defined color variable for each color used in each element (text color, background color, border color, etc.). Gradients of a defined default color variable, or altering the transparency of a defined default color variable, in an element’s color will not carry over to a different mode. Meaning, if you have a defined default “Primary Contrast” color variable with the color #000000 (black) and your corresponding dark mode color of this variable is #ffffff (white), and you use this color variable to style, say, the background color of a button, you cannot make the background color of the button 90% of the Primary Contrast’s color and expect the corresponding dark mode color variable to apply when you switch modes. Because 90% of black is a different string (whether the string is HSL, RGB, GBA, or HEX) than 100% of black, your button’s background color is no longer defined by a color variable; it is overridden by this 90% of black in all modes. Thus, when you switch modes, the button’s background color will stay 90% of black.

Also, taking the same example with our “Primary Contrast” color variable above where the assigned default color is black and corresponding dark mode value is white, you must use the color variable for styling every element’s colors. If you use the color picker in bubble to pick black for an element’s color without using the “Primary Contrast” color variable (which is also black), because the color of the element is technically now defined by a hex string (or RGBA, RGB, HSL) that is not assigned to a color variable, even though it is the same color as a default color variable, it will not be overridden by a corresponding dark mode color variable. This is because the color is not a color variable at this point; it is merely a string without an assigned color variable.

I hope this sheds some light on the issues you are experiencing. If any of the above needs further clarification, please feel free to reach out. And if this did not solve your issue, I will likely need read-only access to your app to determine what is exactly causing these issues.