Skip to content

Comments

feat: add dark mode with system preference detection and toggle#47

Merged
doomspork merged 1 commit intomainfrom
doomspork/add-dark-mode
Feb 7, 2026
Merged

feat: add dark mode with system preference detection and toggle#47
doomspork merged 1 commit intomainfrom
doomspork/add-dark-mode

Conversation

@doomspork
Copy link
Member

Summary

  • Implement class-based dark mode using Tailwind v4 with @custom-variant dark
  • Detect system preference via prefers-color-scheme media query
  • Add inline head script to apply theme before page renders (prevents flash)
  • Persist user theme choice to localStorage with toggle button
  • Add ThemeToggle component with sun/moon icons to all pages (header + docs)
  • Apply dark mode styles across all components and sections

Test plan

  • Light mode renders correctly (default on new visitors)
  • Dark mode toggle button appears in header (desktop and mobile)
  • Clicking toggle switches between light/dark
  • Theme preference persists after page reload
  • System preference is respected when no localStorage entry exists
  • All text remains readable in both modes
  • No flash of light mode on dark mode visitors
  • Docs pages render properly in both themes
  • Navigation colors update correctly on scroll in both modes

Add class-based dark mode using Tailwind v4's @custom-variant directive.
Detect system preference via prefers-color-scheme with an inline head
script to prevent FOUC. Persist user choice in localStorage. Add a
ThemeToggle component with sun/moon icons to the header on all pages.
@doomspork doomspork requested a review from a team as a code owner February 7, 2026 18:45
Copilot AI review requested due to automatic review settings February 7, 2026 18:45
@doomspork doomspork merged commit d6c2b25 into main Feb 7, 2026
5 checks passed
@doomspork doomspork deleted the doomspork/add-dark-mode branch February 7, 2026 18:46
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds class-based dark mode across the Astro/Tailwind site, including early theme application to prevent FOUC, persisted user preference, and updated component styling to support both themes.

Changes:

  • Introduces a Tailwind v4 custom dark variant driven by a .dark class on the root element.
  • Adds an inline head script to set initial theme from localStorage or system preference, plus a reusable ThemeToggle component.
  • Updates navigation and multiple UI components/pages with dark: styles for consistent dark-mode appearance.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/styles/global.css Defines Tailwind custom dark variant targeting .dark class usage.
src/layouts/Layout.astro Adds inline pre-render theme detection + dark-mode body classes.
src/components/ThemeToggle.astro New toggle button + client script to toggle .dark and persist choice.
src/components/Navigation.astro Adds toggle to header; updates scroll-aware nav styling for dark mode.
src/pages/projects/index.astro Adds toggle and dark-mode styling to docs/projects index header and cards.
src/layouts/DocsLayout.astro Adds toggle to docs header and applies dark-mode styling to docs nav/sidebar controls.
src/components/SectionHeading.astro Adds dark-mode typography colors.
src/components/ProjectGrid.astro Adds dark-mode section background.
src/components/ProjectCard.astro Adds dark-mode card/badge/text styles.
src/components/HexDocsLink.astro Adds dark-mode button styling.
src/components/GetInvolved.astro Adds dark-mode section/card/icon/text styles.
src/components/Footer.astro Tweaks dark-mode footer background/border.
src/components/DocsSidebar.astro Adds dark-mode link/section/divider styles.
src/components/DocsNavigation.astro Adds dark-mode borders/backgrounds/text for prev/next navigation.
src/components/DocsContent.astro Enables dark:prose-invert and dark-mode link/code/pre styling.
src/components/DocsBreadcrumb.astro Adds dark-mode breadcrumb text/icon/link styles.
src/components/Badge.astro Adds dark-mode badge background/text colors.
src/components/About.astro Adds dark-mode section/text/feature-card styles.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +38 to +42
document.documentElement.classList.toggle(
"dark",
localStorage.theme === "dark" ||
(!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches),
);
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline theme-detection script directly accesses localStorage and window.matchMedia. In some browsers/privacy modes, localStorage access can throw a SecurityError, which would break early theme application and can log noisy errors. Consider wrapping storage access in a try/catch and using localStorage.getItem('theme') (or treating failures as “no stored preference”) before applying the dark class.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +11
<button
type="button"
class="theme-toggle rounded-lg p-1.5 transition-colors hover:bg-slate-200 dark:hover:bg-slate-700"
aria-label="Toggle dark mode"
>
<Icon name="lucide:sun" class="h-5 w-5 hidden dark:block" />
<Icon name="lucide:moon" class="h-5 w-5 block dark:hidden" />
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ThemeToggle is a persistent on/off control, but the button doesn’t expose state to assistive tech (e.g., no aria-pressed / role="switch" + aria-checked). This makes it hard for screen-reader users to know whether dark mode is currently enabled. Update the script to set an explicit state attribute on load and on toggle, and consider updating the accessible label accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +19
<script>
document.querySelectorAll(".theme-toggle").forEach((btn) => {
btn.addEventListener("click", () => {
const isDark = document.documentElement.classList.toggle("dark");
localStorage.theme = isDark ? "dark" : "light";
});
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The theme toggle click handler writes to localStorage without guarding for storage being unavailable/blocked. If localStorage throws (privacy mode / blocked storage), the click handler will error and leave the UI in a partially updated state. Consider wrapping the localStorage write in try/catch and falling back to only toggling the class when storage isn’t available.

Copilot uses AI. Check for mistakes.
Comment on lines 102 to 105
nav.querySelectorAll(".nav-brand").forEach((el) => {
el.classList.remove("text-white");
el.classList.add("text-slate-900");
el.classList.add(isDark ? "text-white" : "text-slate-900");
});
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In updateNav, .nav-brand only removes text-white but doesn’t remove text-slate-900 when switching to dark mode while scrolled. This can leave both classes on the element and makes the final color depend on class insertion order. Prefer explicitly removing both mutually-exclusive classes (or using classList.toggle with a boolean) before adding the correct one.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant