diff --git a/README.md b/README.md
index 04e670c..5e32630 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ A simple, community-maintained open-source task tracker. Add tasks, check them o
- Single-page, zero-build static web app
- Tasks persist in `localStorage` — no account, no server, no tracking
- Keyboard-friendly (more shortcuts coming, see #5)
-- Light & dark themes (dark coming, see #1)
+- Light and dark themes
- MIT licensed
## Quick start
diff --git a/index.html b/index.html
index 7309515..d26ac44 100644
--- a/index.html
+++ b/index.html
@@ -4,12 +4,39 @@
TaskForge — open-source task tracker
+
diff --git a/src/app.js b/src/app.js
index fe0e9c1..a3268cb 100644
--- a/src/app.js
+++ b/src/app.js
@@ -5,9 +5,24 @@ const form = document.getElementById("task-form");
const input = document.getElementById("task-input");
const list = document.getElementById("task-list");
const emptyState = document.getElementById("empty-state");
+const themeToggle = document.getElementById("theme-toggle");
let tasks = load();
+function currentTheme() {
+ return document.documentElement.dataset.theme === "dark" ? "dark" : "light";
+}
+
+function setTheme(theme, persist = false) {
+ document.documentElement.dataset.theme = theme;
+ if (persist) {
+ window.localStorage.setItem("taskforge.theme", theme);
+ }
+ const nextTheme = theme === "dark" ? "light" : "dark";
+ themeToggle.setAttribute("aria-label", `Switch to ${nextTheme} theme`);
+ themeToggle.setAttribute("aria-pressed", String(theme === "dark"));
+}
+
function render() {
list.innerHTML = "";
if (tasks.length === 0) {
@@ -60,4 +75,9 @@ form.addEventListener("submit", (e) => {
render();
});
+themeToggle.addEventListener("click", () => {
+ setTheme(currentTheme() === "dark" ? "light" : "dark", true);
+});
+
+setTheme(currentTheme());
render();
diff --git a/styles.css b/styles.css
index 2fa1e18..6464ee6 100644
--- a/styles.css
+++ b/styles.css
@@ -6,7 +6,25 @@
--accent: #0969da;
--accent-fg: #ffffff;
--done: #8c959f;
+ --panel: #f6f8fa;
+ --danger: #cf222e;
+ --danger-bg: rgba(207, 34, 46, 0.08);
--radius: 6px;
+ color-scheme: light;
+}
+
+:root[data-theme="dark"] {
+ --bg: #0d1117;
+ --fg: #e6edf3;
+ --muted: #8b949e;
+ --border: #30363d;
+ --accent: #2f81f7;
+ --accent-fg: #ffffff;
+ --done: #6e7681;
+ --panel: #161b22;
+ --danger: #ff7b72;
+ --danger-bg: rgba(248, 81, 73, 0.14);
+ color-scheme: dark;
}
* {
@@ -24,14 +42,23 @@ body {
.app-header {
padding: 2rem 1.25rem 1rem;
- text-align: center;
+ background: var(--panel);
border-bottom: 1px solid var(--border);
}
+.header-content {
+ max-width: 36rem;
+ margin: 0 auto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
.app-header h1 {
margin: 0;
font-size: 1.75rem;
- letter-spacing: -0.01em;
+ letter-spacing: 0;
}
.tagline {
@@ -40,6 +67,38 @@ body {
font-size: 0.95rem;
}
+.theme-toggle {
+ flex: 0 0 auto;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ min-height: 2.25rem;
+ padding: 0.45rem 0.7rem;
+ font: inherit;
+ color: var(--fg);
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ cursor: pointer;
+}
+
+.theme-toggle:hover {
+ border-color: var(--accent);
+}
+
+.theme-toggle:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+}
+
+.theme-toggle-icon {
+ width: 1rem;
+ height: 1rem;
+ border: 2px solid currentColor;
+ border-radius: 50%;
+ background: linear-gradient(90deg, currentColor 50%, transparent 50%);
+}
+
.app-main {
max-width: 36rem;
margin: 0 auto;
@@ -98,6 +157,7 @@ body {
padding: 0.6rem 0.75rem;
border: 1px solid var(--border);
border-radius: var(--radius);
+ background: var(--panel);
}
.task-item.done .task-title {
@@ -122,8 +182,8 @@ body {
}
.task-delete:hover {
- color: #cf222e;
- background: rgba(207, 34, 46, 0.08);
+ color: var(--danger);
+ background: var(--danger-bg);
}
.empty-state {
@@ -142,3 +202,10 @@ body {
.app-footer a {
color: inherit;
}
+
+@media (max-width: 34rem) {
+ .header-content {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+}