Last active
December 25, 2025 18:57
-
-
Save fairjm/ad3f92f7bd1a3bba73461eeadb39dd47 to your computer and use it in GitHub Desktop.
Astro + Shadcn UI: Persistent Theme Toggle with View Transitions. Resolves the issue where the light/dark mode state resets or fails to initialize after navigating between pages in Astro using the ClientRouter(View Transitions).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| import { ModeToggle } from "./mode-toggle"; | |
| --- | |
| <header> | |
| <nav> | |
| <!-- <Menu /> --> | |
| <div class="flex justify-between"> | |
| <ModeToggle client:load /> | |
| </div> | |
| <Navigation /> | |
| </nav> | |
| </header> | |
| <script is:inline> | |
| function setTheme() { | |
| const getThemePreference = () => { | |
| if ( | |
| typeof localStorage !== "undefined" && | |
| localStorage.getItem("theme") | |
| ) { | |
| return localStorage.getItem("theme"); | |
| } | |
| return window.matchMedia("(prefers-color-scheme: dark)").matches | |
| ? "dark" | |
| : "light"; | |
| }; | |
| const isDark = getThemePreference() === "dark"; | |
| document.documentElement.classList[isDark ? "add" : "remove"]("dark"); | |
| console.log("theme set to", isDark ? "dark" : "light"); | |
| // don't use it | |
| // if (typeof localStorage !== "undefined") { | |
| // const observer = new MutationObserver(() => { | |
| // const isDark = document.documentElement.classList.contains("dark"); | |
| // localStorage.setItem("theme", isDark ? "dark" : "light"); | |
| // }); | |
| // observer.observe(document.documentElement, { | |
| // attributes: true, | |
| // attributeFilter: ["class"], | |
| // }); | |
| // } | |
| } | |
| document.addEventListener("astro:after-swap", () => { | |
| setTheme(); | |
| console.log("page loaded"); | |
| }); | |
| setTheme(); | |
| </script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Moon, Sun } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { | |
| DropdownMenu, | |
| DropdownMenuContent, | |
| DropdownMenuItem, | |
| DropdownMenuTrigger, | |
| } from "@/components/ui/dropdown-menu"; | |
| import { useEffect, useState } from "react"; | |
| export function ModeToggle() { | |
| const [theme, setThemeState] = useState<"theme-light" | "dark" | "system">( | |
| () => { | |
| if (typeof window !== "undefined") { | |
| const isDark = document.documentElement.classList.contains("dark"); | |
| return isDark ? "dark" : "theme-light"; | |
| } | |
| return "theme-light"; | |
| } | |
| ); | |
| useEffect(() => { | |
| console.log("theme: ", theme); | |
| }, []); | |
| const setTheme = (newTheme: "theme-light" | "dark" | "system") => { | |
| setThemeState(newTheme); | |
| // update localStorage | |
| const themeValue = newTheme === "theme-light" ? "light" : newTheme; | |
| localStorage.setItem("theme", themeValue); | |
| // update DOM | |
| const isDark = | |
| newTheme === "dark" || | |
| (newTheme === "system" && | |
| window.matchMedia("(prefers-color-scheme: dark)").matches); | |
| document.documentElement.classList[isDark ? "add" : "remove"]("dark"); | |
| }; | |
| return ( | |
| <DropdownMenu> | |
| <DropdownMenuTrigger asChild> | |
| <Button variant="outline" size="icon"> | |
| <Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" /> | |
| <Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" /> | |
| <span className="sr-only">Toggle theme</span> | |
| </Button> | |
| </DropdownMenuTrigger> | |
| <DropdownMenuContent align="end"> | |
| <DropdownMenuItem onClick={() => setTheme("theme-light")}> | |
| Light | |
| </DropdownMenuItem> | |
| <DropdownMenuItem onClick={() => setTheme("dark")}> | |
| Dark | |
| </DropdownMenuItem> | |
| <DropdownMenuItem onClick={() => setTheme("system")}> | |
| System | |
| </DropdownMenuItem> | |
| </DropdownMenuContent> | |
| </DropdownMenu> | |
| ); | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Original code: https://ui.shadcn.com/docs/dark-mode/astro
But it doesn't work with ClientRouter (View Transitions)