From 04b96fb36c4008e7f39e6c74893d61ac0cd0dcba Mon Sep 17 00:00:00 2001
From: Bryce Kwon
Date: Wed, 15 Jul 2026 10:43:24 -1000
Subject: Add a light and dark theme toggle
The button is emitted hidden and only revealed by the script, so a
browser without JavaScript never shows a control that cannot work and
the page keeps tracking the system theme.
---
assets/cgit.css | 54 +++++++++++++++++++++++++++++++++++++++++++++++-----
assets/cgit.js | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
source/ui-shared.c | 3 +++
3 files changed, 108 insertions(+), 5 deletions(-)
diff --git a/assets/cgit.css b/assets/cgit.css
index 73b61f1..a65bc49 100644
--- a/assets/cgit.css
+++ b/assets/cgit.css
@@ -2,8 +2,9 @@
* cgit.css: styling for the cgit web interface
*
* Colours are driven by CSS custom properties via light-dark(), so the
- * whole theme follows the reader's system preference. All rules stay
- * scoped under div#cgit so cgit can be embedded in another page.
+ * whole theme follows the reader's system preference and can be flipped
+ * with the header toggle (which sets data-theme on div#cgit). All rules
+ * stay scoped under div#cgit so cgit can be embedded in another page.
*/
div#cgit {
@@ -94,6 +95,10 @@ div#cgit {
background: var(--bg);
}
+/* Explicit theme choices made through the header toggle. */
+div#cgit[data-theme="light"] { color-scheme: light; }
+div#cgit[data-theme="dark"] { color-scheme: dark; }
+
/* On a standalone cgit page, paint the page background to match the app
* and drop the default body margin so the interface is part of the page
* rather than a card floating on a mismatched canvas. Scoped to the
@@ -188,10 +193,10 @@ div#cgit button:focus-visible {
div#cgit #header {
display: grid;
- grid-template-columns: auto 1fr auto;
+ grid-template-columns: auto 1fr auto auto;
grid-template-areas:
- "logo heading switch"
- "logo desc owner";
+ "logo heading switch theme"
+ "logo desc owner .";
align-items: center;
column-gap: 10px;
width: 100%;
@@ -199,6 +204,45 @@ div#cgit #header {
margin-bottom: 0;
}
+div#cgit #header .theme-toggle {
+ grid-area: theme;
+ justify-self: end;
+ align-self: center;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: var(--control-h);
+ height: var(--control-h);
+ padding: 0;
+ color: var(--muted);
+ background-color: var(--surface);
+ border: solid 1px var(--border);
+ border-radius: 5px;
+ cursor: pointer;
+ line-height: 1;
+}
+
+/* Keep the button hidden until the script reveals it, since the display
+ * rule above would otherwise override the hidden attribute. */
+div#cgit #header .theme-toggle[hidden] {
+ display: none;
+}
+
+div#cgit #header .theme-toggle:hover {
+ color: var(--fg);
+ border-color: var(--border-mid);
+}
+
+div#cgit #header .theme-toggle svg {
+ width: 1.1em;
+ height: 1.1em;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
div#cgit #header .logo {
grid-area: logo;
align-self: start;
diff --git a/assets/cgit.js b/assets/cgit.js
index df3ad4e..85826b4 100644
--- a/assets/cgit.js
+++ b/assets/cgit.js
@@ -66,3 +66,59 @@ document.addEventListener("DOMContentLoaded", function() {
}, false);
})();
+
+/* Colour theme toggle: cycles auto -> light -> dark and remembers the
+ * choice. "auto" leaves the page following the system preference via CSS. */
+
+(function () {
+
+var KEY = "cgit-theme";
+var ORDER = [ "auto", "light", "dark" ];
+var ICONS = {
+ auto: '',
+ light: '',
+ dark: ''
+};
+
+function saved() {
+ try { return localStorage.getItem(KEY); } catch (e) { return null; }
+}
+
+function persist(value) {
+ try { localStorage.setItem(KEY, value); } catch (e) { }
+}
+
+function apply(theme, root, btn) {
+ if (theme === "auto")
+ root.removeAttribute("data-theme");
+ else
+ root.setAttribute("data-theme", theme);
+ /* Keep the standalone page background in sync with the choice. */
+ document.documentElement.style.colorScheme = (theme === "auto") ? "" : theme;
+ if (btn) {
+ btn.innerHTML = ICONS[theme];
+ btn.title = "Colour theme: " + theme;
+ }
+}
+
+document.addEventListener("DOMContentLoaded", function () {
+ var root = document.getElementById("cgit");
+ if (!root)
+ return;
+ var btn = root.querySelector(".theme-toggle");
+ var theme = saved();
+ if (ORDER.indexOf(theme) < 0)
+ theme = "auto";
+
+ apply(theme, root, btn);
+ if (!btn)
+ return;
+ btn.hidden = false;
+ btn.addEventListener("click", function () {
+ theme = ORDER[(ORDER.indexOf(theme) + 1) % ORDER.length];
+ persist(theme);
+ apply(theme, root, btn);
+ });
+}, false);
+
+})();
diff --git a/source/ui-shared.c b/source/ui-shared.c
index 83454fe..88e5075 100644
--- a/source/ui-shared.c
+++ b/source/ui-shared.c
@@ -1082,6 +1082,9 @@ static void print_header(void)
html_txt(ctx.cfg.root_desc);
html("
\n");
}
+
+ html("\n");
html("\n");
}
--
cgit v2.8.0