diff --git a/debian/copyright b/debian/copyright
index 2d583c133..8e37e5817 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -38,6 +38,11 @@ Copyright: Marie Van den Broeck (https://thenounproject.com/marie49/)
Comment: https://thenounproject.com/icon/162372/
License: CC-BY-SA-3.0
+Files: static/themes/default/js/color-modes.js
+Copyright: 2011-2025 The Bootstrap Authors
+Comment: https://getbootstrap.com/docs/5.3/customize/color-modes/
+License: CC-BY-3.0
+
Files: plinth/modules/bepasty/static/icons/bepasty.svg
Copyright: (c) 2014 by the Bepasty Team, see the AUTHORS file.
Comment: https://github.com/bepasty/bepasty-server/blob/master/src/bepasty/static/app/bepasty.svg
diff --git a/plinth/templates/base.html b/plinth/templates/base.html
index 2a074b5a8..b727f68fb 100644
--- a/plinth/templates/base.html
+++ b/plinth/templates/base.html
@@ -58,6 +58,9 @@
{% endif %}
+
+
@@ -148,6 +151,8 @@
{% include "notifications-dropdown.html" %}
+ {% include "theme-menu.html" %}
+
{% include "help-menu.html" %}
diff --git a/static/themes/default/css/main.css b/static/themes/default/css/main.css
index b8bb67ff6..d5e8adc5c 100644
--- a/static/themes/default/css/main.css
+++ b/static/themes/default/css/main.css
@@ -522,6 +522,10 @@ footer {
border-bottom: var(--freedombox-navbar-color) 3px solid;
}
+.main-header .dropdown-menu .active .fa-check {
+ display: block !important;
+}
+
/* Breadcrumbs */
.breadcrumb-item {
--bs-breadcrumb-divider: ">";
diff --git a/static/themes/default/js/color-modes.js b/static/themes/default/js/color-modes.js
new file mode 100644
index 000000000..efef6dbf3
--- /dev/null
+++ b/static/themes/default/js/color-modes.js
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: CC-BY-3.0
+/*
+ This file is part of FreedomBox. Color mode toggler for Bootstrap's docs
+ (https://getbootstrap.com/). Copyright 2011-2025 The Bootstrap Authors.
+
+ @licstart The following is the entire license notice for the
+ JavaScript code in this page.
+
+ Licensed under the Creative Commons Attribution 3.0 Unported License.
+
+ @licend The above is the entire license notice
+ for the JavaScript code in this page.
+*/
+
+(() => {
+ 'use strict';
+
+ const getStoredTheme = () => localStorage.getItem('theme');
+ const setStoredTheme = theme => localStorage.setItem('theme', theme);
+
+ const getBrowserTheme = () => {
+ return window.matchMedia('(prefers-color-scheme: dark)')
+ .matches ? 'dark' : 'light';
+ };
+
+ const getPreferredTheme = () => {
+ const storedTheme = getStoredTheme();
+ if (storedTheme) {
+ return storedTheme;
+ }
+
+ return getBrowserTheme();
+ };
+
+ const setTheme = (theme) => {
+ if (theme === 'auto') {
+ theme = getBrowserTheme();
+ }
+ document.documentElement.setAttribute('data-bs-theme', theme);
+ };
+
+ setTheme(getPreferredTheme());
+
+ const showActiveTheme = (theme, focus = false) => {
+ const themeSwitcher = document.querySelector('#id_theme_menu_link');
+
+ if (!themeSwitcher) {
+ return;
+ }
+
+ const themeSwitcherText = document.querySelector('#id_toggle_theme_text');
+ const activeThemeIcon = document.querySelector('#id_active_theme_icon');
+ const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
+ const iconOfActiveBtn = btnToActive.dataset.bsIconValue;
+
+ document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
+ element.classList.remove('active');
+ element.setAttribute('aria-pressed', 'false');
+ const iconOfBtn = element.dataset.bsIconValue;
+ if (activeThemeIcon.classList.contains(iconOfBtn)) {
+ activeThemeIcon.classList.remove(iconOfBtn);
+ }
+ });
+
+ btnToActive.classList.add('active');
+ btnToActive.setAttribute('aria-pressed', 'true');
+ activeThemeIcon.classList.add(iconOfActiveBtn);
+ const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`;
+ themeSwitcher.setAttribute('title', themeSwitcherLabel);
+
+ if (focus) {
+ themeSwitcher.focus();
+ };
+ };
+
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
+ const storedTheme = getStoredTheme();
+ if (storedTheme !== 'light' && storedTheme !== 'dark') {
+ setTheme(getPreferredTheme());
+ }
+ });
+
+ window.addEventListener('DOMContentLoaded', () => {
+ showActiveTheme(getPreferredTheme());
+
+ document.querySelectorAll('[data-bs-theme-value]')
+ .forEach(toggle => {
+ toggle.addEventListener('click', () => {
+ const theme = toggle.getAttribute('data-bs-theme-value');
+ setStoredTheme(theme);
+ setTheme(theme);
+ showActiveTheme(theme, true);
+ });
+ });
+ });
+})();