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:
co-authored by
Claude Opus 4.6
parent
e2e10e9d9b
commit
a978df4af9
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 + "')";
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user