add dark/light mode toggle with OS preference detection

Uses Bootstrap 5.3 data-bs-theme attribute for automatic component
theming. Adds CSS custom properties for dark mode, a toggle button
in the header, localStorage persistence, and a flash-prevention
script. Background image overlay adjusts per theme.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mark Eaton
2026-02-21 23:27:53 -05:00
co-authored by Claude Opus 4.6
parent e2e10e9d9b
commit a978df4af9
3 changed files with 142 additions and 10 deletions
+67 -9
View File
@@ -3,11 +3,27 @@
--kbccblue: #003466; --kbccblue: #003466;
--kbccorange: #ff4e00; --kbccorange: #ff4e00;
--text: #111; --text: #111;
--bg: #fff;
--bg-surface: #fff;
--hover-dark: #444;
--focus-outline: #0056b3;
--link-color: black;
}
[data-bs-theme="dark"] {
--text: #e0e0e0;
--bg: #1a1a2e;
--bg-surface: #25253e;
--hover-dark: #555;
--focus-outline: #6ea8fe;
--link-color: #ccc;
} }
body { body {
color: var(--text); color: var(--text);
background-color: var(--bg);
overflow-x: hidden; overflow-x: hidden;
transition: background-color 0.3s ease, color 0.3s ease;
} }
a, a,
@@ -53,14 +69,14 @@
#hamburger:active { #hamburger:active {
border: none; border: none;
outline: none; outline: none;
background-color: #fff; background-color: var(--bg-surface);
color: var(--kbccblue); color: var(--kbccblue);
} }
#hamburger::after { #hamburger::after {
display: none; display: none;
} }
#hamburger-ul > li > .searchmenu { #hamburger-ul > li > .searchmenu {
background-color: #fff; background-color: var(--bg-surface);
} }
#hamburger-ul { #hamburger-ul {
padding: 25px 0; padding: 25px 0;
@@ -133,7 +149,7 @@
/* Navigation dropdown toggles */ /* Navigation dropdown toggles */
.dropdown-toggle:focus, .dropdown-toggle:focus,
.dropdown-toggle:focus-visible { .dropdown-toggle:focus-visible {
outline: 3px solid #0056b3; /* high-contrast blue */ outline: 3px solid var(--focus-outline); /* high-contrast blue */
outline-offset: 2px; outline-offset: 2px;
} }
@@ -153,8 +169,8 @@
/* style navbar */ /* style navbar */
nav.navbar-light.main-nav-container { nav.navbar-light.main-nav-container {
background-color: white !important; background-color: var(--bg-surface) !important;
background: white !important; background: var(--bg-surface) !important;
border: none; border: none;
font-size: 1.4em; font-size: 1.4em;
color: var(--text); color: var(--text);
@@ -167,7 +183,7 @@
.nav .show > a, .nav .show > a,
.nav .show > a:focus, .nav .show > a:focus,
.nav .show > a:hover { .nav .show > a:hover {
background-color: #fff; background-color: var(--bg-surface);
} }
.nav > li > a:hover { .nav > li > a:hover {
outline: none; outline: none;
@@ -252,7 +268,7 @@
margin-top: 10px; margin-top: 10px;
} }
#advanced > strong > a { #advanced > strong > a {
color: black; color: var(--link-color);
} }
/* add flex so that blue box-shadows line up */ /* add flex so that blue box-shadows line up */
#flex-search-form { #flex-search-form {
@@ -310,7 +326,7 @@
margin-top: 4px; margin-top: 4px;
} }
.nav-divs-by-jumbotron:hover { .nav-divs-by-jumbotron:hover {
background-color: #444; background-color: var(--hover-dark);
} }
#eq_32886 { #eq_32886 {
background-color: var(--kbccblue); background-color: var(--kbccblue);
@@ -318,7 +334,7 @@
} }
.nav-divs-by-jumbotron:hover > h2 > #eq_32886, .nav-divs-by-jumbotron:hover > h2 > #eq_32886,
#eq_32886:hover { #eq_32886:hover {
background-color: #444; background-color: var(--hover-dark);
} }
#bookastudyroom { #bookastudyroom {
cursor: pointer; cursor: pointer;
@@ -473,6 +489,48 @@
max-width: 150px; max-width: 150px;
} }
/* dark mode toggle button */
#theme-toggle {
float: right;
background: none;
border: 2px solid var(--kbccblue);
color: var(--kbccblue);
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 1.2em;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
margin-top: 5px;
}
#theme-toggle:hover {
background-color: var(--kbccblue);
color: #fff;
}
[data-bs-theme="dark"] #theme-toggle {
border-color: #ffc107;
color: #ffc107;
}
[data-bs-theme="dark"] #theme-toggle:hover {
background-color: #ffc107;
color: #1a1a2e;
}
/* card body dark mode background */
.card-body {
background-color: var(--bg-surface);
transition: background-color 0.3s ease;
}
/* dropdown menu dark mode background */
.dropdown-menu {
background-color: var(--bg-surface);
transition: background-color 0.3s ease;
}
/* media queries */ /* media queries */
@media only screen and (min-width: 1px) { @media only screen and (min-width: 1px) {
+19
View File
@@ -1,4 +1,15 @@
<body class="s-lib-public-body"> <body class="s-lib-public-body">
<script>
// Prevent flash of wrong theme by applying saved/OS preference immediately
(function() {
var stored = localStorage.getItem("kbcc-theme");
var prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
var isDark = stored ? stored === "dark" : prefersDark;
if (isDark) {
document.body.setAttribute("data-bs-theme", "dark");
}
})();
</script>
<!-- Google Tag Manager (noscript) --> <!-- Google Tag Manager (noscript) -->
<noscript> <noscript>
<iframe <iframe
@@ -65,6 +76,14 @@
<div id="login-text">Login</div> <div id="login-text">Login</div>
<div id="login-icon"><i class="fas fa-user" aria-hidden="true"></i></div> <div id="login-icon"><i class="fas fa-user" aria-hidden="true"></i></div>
</a> </a>
<button
id="theme-toggle"
@click="toggleTheme"
:aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
type="button"
>
<i :class="isDark ? 'fas fa-sun' : 'fas fa-moon'" aria-hidden="true"></i>
</button>
<img <img
id="library-logo" id="library-logo"
class="image-fail" class="image-fail"
+56 -1
View File
@@ -222,6 +222,7 @@ const app1 = createApp({
}, },
data() { data() {
return { return {
isDark: false,
hamburger: [ hamburger: [
{ {
link: "https://cuny-kb.primo.exlibrisgroup.com/discovery/search?tab=Everything&vid=01CUNY_KB:CUNY_KB&lang=en", link: "https://cuny-kb.primo.exlibrisgroup.com/discovery/search?tab=Everything&vid=01CUNY_KB:CUNY_KB&lang=en",
@@ -454,7 +455,56 @@ const app1 = createApp({
}, },
}; };
}, },
methods: {
toggleTheme() {
this.isDark = !this.isDark;
this.applyTheme(this.isDark);
localStorage.setItem("kbcc-theme", this.isDark ? "dark" : "light");
},
applyTheme(isDark) {
if (isDark) {
document.body.setAttribute("data-bs-theme", "dark");
} else {
document.body.removeAttribute("data-bs-theme");
}
this.updateBackgroundOverlay(isDark);
},
updateBackgroundOverlay(isDark) {
var el = document.querySelector(".kbcc-background-image");
if (el && el.style.backgroundImage) {
// Extract the url() part from the existing background-image
var match = el.style.backgroundImage.match(/url\([^)]+\)/);
if (match) {
var overlay = isDark
? "rgba(0,0,0,0.4)"
: "rgba(255,255,255,0.6)";
el.style.backgroundImage =
"linear-gradient(to bottom, " + overlay + " 0%, " + overlay + " 100%), " + match[0];
}
}
}
},
mounted() { mounted() {
// Theme initialization
var stored = localStorage.getItem("kbcc-theme");
var prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
if (stored) {
this.isDark = stored === "dark";
} else {
this.isDark = prefersDark;
}
this.applyTheme(this.isDark);
// Listen for OS preference changes (only applies when no manual override)
var self = this;
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", function(e) {
if (!localStorage.getItem("kbcc-theme")) {
self.isDark = e.matches;
self.applyTheme(self.isDark);
}
});
// Dynamic aria-expanded for hamburger menu (WCAG 4.1.2) // Dynamic aria-expanded for hamburger menu (WCAG 4.1.2)
const hamburger = document.getElementById('hamburger'); const hamburger = document.getElementById('hamburger');
const hamburgerContainer = document.getElementById('hamburger-container'); const hamburgerContainer = document.getElementById('hamburger-container');
@@ -490,8 +540,13 @@ app2.mount("#app2");
var picked = images[Math.floor(Math.random() * images.length)]; var picked = images[Math.floor(Math.random() * images.length)];
var el = document.querySelector(".kbcc-background-image"); var el = document.querySelector(".kbcc-background-image");
if (el) { if (el) {
// Check theme for correct overlay color
var stored = localStorage.getItem("kbcc-theme");
var prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
var isDark = stored ? stored === "dark" : prefersDark;
var overlay = isDark ? "rgba(0,0,0,0.4)" : "rgba(255,255,255,0.6)";
el.style.backgroundImage = el.style.backgroundImage =
"linear-gradient(to bottom, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0.6) 100%), url('" + picked + "')"; "linear-gradient(to bottom, " + overlay + " 0%, " + overlay + " 100%), url('" + picked + "')";
} }
})(); })();