Theme Pair Toggle
A tiny, no-flicker light/dark switch that swaps between your chosen DaisyUI pair.
It shows the correct icon on first paint (no “sun flash” in dark mode) and animates the moon on toggle.
You can swap out the existing ThemeSwitcherWithPreviews in src/components/navigation/NavBar.astro
Quick start (3 steps)
- Pick your pair (example below uses
cupcake
as light anddracula
as dark). - In the global CSS (/src/styles/global.css) loaded on every page simply update theme names to match your pair.
/* Show the correct glyph purely from <html data-theme> – no theme-name assumptions */
:root[data-theme="dracula"] .theme-toggle .sun-icon {
opacity: 0;
pointer-events: none;
transform: rotate(180deg);
}
:root[data-theme="dracula"] .theme-toggle .moon-icon {
opacity: 1;
pointer-events: auto;
}
/* Any theme that is NOT your dark theme => treat as light */
:root:not([data-theme="dracula"]) .theme-toggle .sun-icon {
opacity: 1;
pointer-events: auto;
}
:root:not([data-theme="dracula"]) .theme-toggle .moon-icon {
opacity: 0;
pointer-events: none;
}
/* Replace "light"/"dracula" above with your actual pair, e.g. cupcake/dracula */
/* Moon entrance animation (only on explicit toggle to dark) */
@media (prefers-reduced-motion: no-preference) {
@keyframes moonPop {
0% {
transform: scale(0.8) rotate(-12deg);
}
60% {
transform: scale(1.05) rotate(0deg);
}
100% {
transform: scale(1) rotate(0deg);
}
}
/* No fill mode → when the class is removed, it won't “stick” */
.theme-toggle .moon-animate {
animation: moonPop 280ms ease-out;
will-change: transform;
}
}
Import
and add in place of the existing theme switcher insrc/components/navigation/NavBar.astro
<!-- <ThemeSwitcherWithPreviews client:load /> -->
<ThemePairToggle client:load />
- (Recommended) Keep your early head script in the
<head>
that sets<html data-theme="…">
fromlocalStorage("theme")
or system preference. That prevents page flicker; the CSS above ensures the correct icon is visible immediately.
Usage (Astro)
Use the React component directly in .astro
or .mdx
with client:load
:
Default pair (cupcake ↔ dracula)
<ThemePairToggle lightTheme="cupcake" darkTheme="dracula" client:load />
Smaller / larger
<div className="flex items-center gap-4">
<ThemePairToggle lightTheme="cupcake" darkTheme="dracula" size={5} client:load />
<ThemePairToggle lightTheme="cupcake" darkTheme="dracula" size={8} client:load />
</div>
Custom pair (light ↔ business)
<ThemePairToggle lightTheme="light" darkTheme="business" client:load />
Inherit color from context
<div className="p-4 rounded-xl bg-base-200 text-secondary">
<ThemePairToggle lightTheme="cupcake" darkTheme="dracula" client:load />
</div>
Props
lightTheme
(string): DaisyUI theme used for light.darkTheme
(string): DaisyUI theme used for dark.storageKey
(string, default"theme"
): persisted key (stays in sync with your full switcher).size
(number, default6
): Tailwind size for a square icon (h-{size} w-{size}
).className
(string): extra classes (e.g.,text-base-content
).
The toggle:
- reads the initial theme before first render from
localStorage("theme")
and<html data-theme>
, - updates
<html data-theme>
andlocalStorage
on click, - dispatches a
theme:change
event so multiple toggles stay in sync, - stacks Sun and Moon in a fixed square (no horizontal nudge),
- animates Moon only when entering dark (no animation on page load).
Troubleshooting
- Icon “disappears” on navigation: ensure the global CSS above is loaded on every page and the selectors use your exact pair names (replace
cupcake
/dracula
if different). - Wrong icon on first paint: confirm your early head script sets
<html data-theme="…">
before paint. - Color looks invisible: the icons inherit
currentColor
. Wrap the toggle in a container with a visible text color (e.g.,text-base-content
).
That’s it—drop the CSS in once, choose your pair, and use <ThemePairToggle … />
anywhere.