Calum Harrison logo
  • Home 
  • Tags 
  • About Me 
  1. Home
  2. Posts
  3. Dark mode in Power Pages 🌚

Dark mode in Power Pages 🌚

Posted on March 29, 2025  (Last modified on March 31, 2025) • 5 min read • 995 words
Power Pages   Bootstrap   Design   Dark Mode  
Power Pages   Bootstrap   Design   Dark Mode  
Share via
Calum Harrison
Link copied to clipboard

On this page
  • What we will build
  • Build time
    • Breakdown of css code
    • Breakdown of html code
    • Breakdown of js code
  • Full code
  • Conclusion
Dark mode in Power Pages 🌚

Research by Android Authority found that nearly 82% of smartphone users use dark mode which isn’t surprising given the numerous benefits it offers. Although I might be a bit biased as a dev…

With that in mind, I thought I would share a way to add dark mode to Power Pages as it’s a key feature for improving end-user experience.

What we will build  


There will be a bonus at the end, in that we will store the theme preference in the browser so that when the user returns to the website it remembers their theme preference!

  Important

I am using the LUX theme from Bootswatch so feel free to checkout this blog where I go through how you can apply a custom theme to Power Pages.

Also, make sure you deactivate the basicportaltheme.css as we don’t want this to come through to the design.

Build time  

Copy the below code into your theme.css web file to style the page.

:root {
  --background-color: #ffffff;
  --text-color: #000000;
}

[data-theme='dark'] {
  --background-color: #1a1a1a;
  --text-color: #ffffff;
}

[data-theme='light'] {
  --background-color: #ffffff;
  --text-color: #000000;
}

body {
  background-color: var(--background-color);
  color: var(--text-color);
  transition: background-color 0.5s ease;
}

.nav-link {
  color: var(--text-color);
  transition: inherit;
}

.navbar-brand {
  color: var(--text-color);
  transition: inherit;

}

.bi-moon-stars-fill:hover {

  color: #f0c420;
  transition: color 0.3s ease;

}

.bi-brightness-high-fill:hover {

  color: #fc9601;
  transition: color 0.3s ease;

}

Breakdown of css code  

  • Sets CSS variables for the background and text colours that is used for the data-theme.
  • Control the hover effects and colours for the theme icon.
  • Nice transition to make sure the switch between the themes is not too harsh but more subtle.

For the content and logic of the page replace your headerwebtemplate.html code with the below.

  Note

For the sake of a demo I have added the javascript code to a web template, it’s best practice for reusable functions to be stored in a js file.


<nav class="navbar navbar-expand-lg">
    <div class="container-fluid">
        <div class="navbar-brand" href="#">{{ snippets['Site name'] }}</div>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
            aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav">
                {% assign nav = weblinks['Default'] %}
                {% if nav %}
                {% for link in nav.weblinks %}
                <li class="nav-item">
                    <a class="nav-link" aria-current="page">{{ link.name | escape }}</a>
                </li>
                {% endfor %}
                {% endif %}

            </ul>
            <div class="form-check form-switch ms-auto" id="theme-toggle">
                <i class="bi bi-brightness-high-fill fs-3 pe-5" id="theme-icon"></i>
            </div>
        </div>
    </div>
</nav>

<!-- Script to handle toggling theme dynamically -->

<script>

    const themeIcon = document.getElementById("theme-icon");
    const rootElement = document.documentElement;

    const savedTheme = localStorage.getItem("theme") || "light";

    rootElement.setAttribute("data-theme", savedTheme);

    const setLocalThemeIcon = (localtheme, themeicon) => {

        if (localtheme === "dark") {
            themeicon.classList.remove("bi-brightness-high-fill");
            themeicon.classList.add("bi-moon-stars-fill");

        } else if (localtheme === "light") {

            themeIcon.classList.remove("bi-moon-stars-fill");
            themeIcon.classList.add("bi-brightness-high-fill");
        }
    }

    if (savedTheme) {
        setLocalThemeIcon(savedTheme, themeIcon);

    }

    themeIcon.addEventListener("click", () => {

        // switch Boostrap icon

        if (themeIcon.classList.contains("bi-moon-stars-fill")) {
            themeIcon.classList.remove("bi-moon-stars-fill");
            themeIcon.classList.add("bi-brightness-high-fill");
        } else {
            themeIcon.classList.remove("bi-brightness-high-fill");
            themeIcon.classList.add("bi-moon-stars-fill");
        }

        // set theme 

        const currentTheme = rootElement.getAttribute("data-theme");
        const newTheme = currentTheme === "dark" ? "light" : "dark";
        rootElement.setAttribute("data-theme", newTheme);

        // set localstorage to remember theme and toggle value 
        localStorage.setItem("theme", newTheme);
    });

</script>

Breakdown of html code  

  • Stores the navbar and uses a content snippet to store the website name.
  • Web links are used to output each menu item in the navbar.
  • Theme toggler is set to the right of the navbar and is given identifiers that will be used in the event listeners in the Javascript.

Breakdown of js code  

  • The script retrieves the saved theme (light or dark) from localStorage and applies it to the data-theme attribute on the root element.
  • It sets the appropriate icon (bi-moon-stars-fill for dark, bi-brightness-high-fill for light) based on the current theme.
  • When the theme icon is clicked, it toggles between light and dark themes, updates the icon, and saves the new theme to localStorage for persistence.
  • As we don’t want the end-user to keep having to toggle their preference when they return to the website through a different tab etc, the theme is remembered across sessions using localStorage.

Full code  

:root {
  --background-color: #ffffff;
  --text-color: #000000;
}

[data-theme='dark'] {
  --background-color: #1a1a1a;
  --text-color: #ffffff;
}

[data-theme='light'] {
  --background-color: #ffffff;
  --text-color: #000000;
}

body {
  background-color: var(--background-color);
  color: var(--text-color);
  transition: background-color 0.5s ease;
}

.nav-link {
  color: var(--text-color);
  transition: inherit;
}

.navbar-brand {
  color: var(--text-color);
  transition: inherit;

}

.bi-moon-stars-fill:hover {

  color: #f0c420;
  transition: color 0.3s ease;

}

.bi-brightness-high-fill:hover {

  color: #fc9601;
  transition: color 0.3s ease;

}

<nav class="navbar navbar-expand-lg">
    <div class="container-fluid">
        <div class="navbar-brand" href="#">{{ snippets['Site name'] }}</div>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
            aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav">
                {% assign nav = weblinks['Default'] %}
                {% if nav %}
                {% for link in nav.weblinks %}
                <li class="nav-item">
                    <a class="nav-link" aria-current="page">{{ link.name | escape }}</a>
                </li>
                {% endfor %}
                {% endif %}

            </ul>
            <div class="form-check form-switch ms-auto" id="theme-toggle">
                <i class="bi bi-brightness-high-fill fs-3 pe-5" id="theme-icon"></i>
            </div>
        </div>
    </div>
</nav>

<!-- Script to handle toggling theme dynamically -->

<script>

    const themeIcon = document.getElementById("theme-icon");
    const rootElement = document.documentElement;

    const savedTheme = localStorage.getItem("theme") || "light";

    rootElement.setAttribute("data-theme", savedTheme);

    const setLocalThemeIcon = (localtheme, themeicon) => {

        if (localtheme === "dark") {
            themeicon.classList.remove("bi-brightness-high-fill");
            themeicon.classList.add("bi-moon-stars-fill");

        } else if (localtheme === "light") {

            themeIcon.classList.remove("bi-moon-stars-fill");
            themeIcon.classList.add("bi-brightness-high-fill");
        }
    }

    if (savedTheme) {
        setLocalThemeIcon(savedTheme, themeIcon);

    }

    themeIcon.addEventListener("click", () => {

        // switch Boostrap icon

        if (themeIcon.classList.contains("bi-moon-stars-fill")) {
            themeIcon.classList.remove("bi-moon-stars-fill");
            themeIcon.classList.add("bi-brightness-high-fill");
        } else {
            themeIcon.classList.remove("bi-brightness-high-fill");
            themeIcon.classList.add("bi-moon-stars-fill");
        }

        // set theme 

        const currentTheme = rootElement.getAttribute("data-theme");
        const newTheme = currentTheme === "dark" ? "light" : "dark";
        rootElement.setAttribute("data-theme", newTheme);

        // set localstorage to remember theme and toggle value 
        localStorage.setItem("theme", newTheme);
    });

</script>

To finish it off just add a image to a web page and you should be able to see the header come through.


Conclusion  

I will have a little rant and say this would be so much easier if Power Pages supported Bootstrap 5.3 as this version includes colour modes (dark/light mode)😩

Hopefully, we’ll see this feature in the future, but for now, it’s still possible to implement dark mode with a bit of effort.

As always, thanks for reading and have a great day!

 How to collect user feedback using Hotjar in Power Pages
FREE custom Bootstrap themes for Power Pages 🎨 
On this page:
  • What we will build
  • Build time
    • Breakdown of css code
    • Breakdown of html code
    • Breakdown of js code
  • Full code
  • Conclusion
Follow me on LinkedIn

 
Copyright Β© 2025 Calum Harrison. | Powered by Hinode.
Calum Harrison
Code copied to clipboard