celestial-theme-toggle
v1.0.1
Published
Animated toggle button for light/dark mode with sun/moon transition, particles, glow effects, and optional full-page sky scene with real lunar cycle moon phases
Maintainers
Readme
celestial-theme-toggle
Cinematic theme transition engine for light/dark mode. The entire viewport smoothly transforms from day to night through sunset/sunrise gradient animations, celestial body transitions, and delayed theme switching — not just an animated icon.
Features
- Cinematic Transition — Full-viewport sunset/sunrise gradient animation via
requestAnimationFrame - Delayed Theme Switch — Theme class applied at ~80% of animation, eliminating flash
- Application Dimming — Overlay effect masks the theme switch for seamless visual flow
- Sun Rise / Set — Smooth arc animation: sun rises from below horizon in light mode, sets in dark mode
- Moon Rise / Set — Moon rises in dark mode, sets in light mode
- Real Lunar Cycle — Moon crescent shape follows the actual 29.5-day synodic month
- Twinkling Stars — 40 stars with staggered fade-in and randomized twinkle animations
- Drifting Clouds — Natural cloud movement, transparent at night
- 60 FPS — Uses
requestAnimationFrame, CSS transforms, andwill-changefor smooth performance prefers-reduced-motion— Automatically disables animations when the OS setting requests it- Zero Dependencies — Lunar calculations built-in, all CSS injected at runtime
- Vanilla JS + React — Works with any framework or plain HTML
- localStorage Persistence — Remembers user preference
- System Preference — Respects
prefers-color-scheme
Install
npm install celestial-theme-toggleVanilla JS Usage
Basic
import { initCelestialToggle } from 'celestial-theme-toggle';
// Simple — creates toggle button, mounts to body
initCelestialToggle();With Cinematic Transition
initCelestialToggle({
theme: 'light',
fullPageSky: true,
transitionDuration: 1200, // ms — total animation length
delayThemeSwitch: true, // switch theme at ~80% of animation
animateApplication: true, // dim overlay masks theme switch
animation: 'sunset', // 'sunset' | 'fade' | 'instant'
});All Options
initCelestialToggle({
// Theme
theme: 'system', // 'light' | 'dark' | 'system'
// Toggle Button
buttonPosition: 'bottom-right',
showToggle: true,
// Scene
fullPageSky: true, // render sun, moon, stars, clouds
autoMount: true, // auto-append to document.body
cssVariables: { /* ... */ },
// Transition Engine
transitionDuration: 1000, // animation duration in ms
delayThemeSwitch: true, // delay theme class until ~80%
switchPoint: 0.8, // when to apply theme (0-1)
animateApplication: true, // overlay dimming effect
transitionRoot: 'html', // CSS selector for dimming target
animation: 'sunset', // 'sunset' | 'fade' | 'instant'
});Instance API
const instance = initCelestialToggle({ fullPageSky: true });
instance.getTheme(); // 'light' | 'dark'
instance.getMode(); // 'light' | 'dark' | 'system'
instance.setTheme('dark'); // switch to dark (immediate)
instance.toggle(); // toggle with cinematic transition
instance.onChange((theme) => {
console.log('Theme changed to:', theme);
});
instance.destroy(); // clean up everythingTransition Engine Architecture
User clicks toggle
|
v
TransitionManager.start(targetTheme)
|
v
+-- RAF loop begins --+
| |
v v
Sky gradient Overlay opacity
interpolates increases
through sunset |
colors |
| |
v v
Sun/moon Stars/clouds
animate fade in/out
|
v
Progress >= 80%
|
v
Theme class applied
(data-theme / classList)
|
v
Overlay fades out
|
v
Cleanup & resetHow It Works
- Click — Toggle button click starts the
TransitionManager - RAF Loop —
requestAnimationFramedrives the animation at 60fps - Sky Gradient — Colors interpolate through 4 keyframe stages:
- Sunset: Day → Sunset → Dusk → Night
- Sunrise: Night → Dawn → Sunrise → Day
- Celestial Bodies — Sun/moon translate along Y-axis with eased timing
- Stars — Fade in with staggered delays (35%-85% of progress)
- Clouds — Fade out over 20%-80% of progress
- Overlay — Semi-transparent overlay peaks at ~75% opacity just before theme switch
- Theme Switch — At 80% progress,
data-themeattribute is applied. The overlay hides any visual discontinuity. - Cleanup — Overlay fades out, inline styles are removed, CSS transitions resume
Performance
- Uses
requestAnimationFramefor frame-synchronized updates will-change: transform, opacityon animated elements- Inline styles set only during active animation
- CSS transitions disabled during JS-driven animation
- After cleanup, standard CSS transitions take over
React Usage
import { CelestialToggle, useCelestialTheme } from 'celestial-theme-toggle/react';
function App() {
return (
<CelestialToggle
defaultTheme="dark"
buttonPosition="bottom-right"
transitionDuration={1200}
animateApplication
animation="sunset"
>
<h1>Hello World</h1>
<p>This content smoothly transitions between themes</p>
</CelestialToggle>
);
}React Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| defaultTheme | 'light' \| 'dark' \| 'system' | 'system' | Initial theme mode |
| theme | 'light' \| 'dark' \| 'system' | — | Controlled theme |
| onThemeChange | (theme) => void | — | Theme change callback |
| buttonPosition | 'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left' | 'bottom-right' | Toggle button position |
| showToggle | boolean | true | Show/hide toggle button |
| cssVariables | Record<string, string> | — | CSS custom property overrides |
| children | ReactNode | — | App content |
| className | string | — | Additional wrapper className |
| transitionDuration | number | 1000 | Animation duration in ms |
| delayThemeSwitch | boolean | true | Delay theme class until late in animation |
| animateApplication | boolean | true | Overlay dimming effect |
| transitionRoot | string | 'html' | CSS selector for dimming target |
| animation | 'sunset' \| 'fade' \| 'instant' | 'sunset' | Animation style |
useCelestialTheme Hook
import { useCelestialTheme } from 'celestial-theme-toggle/react';
function MyComponent() {
const { theme, mode, setTheme, toggle } = useCelestialTheme();
return (
<button onClick={toggle}>
Current: {theme}
</button>
);
}useCelestialContext Hook
import { useCelestialContext } from 'celestial-theme-toggle/react';
function ThemeLabel() {
const { theme, toggle } = useCelestialContext();
return <span onClick={toggle}>{theme}</span>;
}Lunar Phase API
import {
getLunarPhase, // "new" | "waxing-crescent" | "first-quarter" | ...
getLunarAge, // days since last new moon (0 to 29.53)
getLunarAgePercent, // 0.0 to 1.0
getMoonIllumination, // 0.0 (dark) to 1.0 (full moon)
getCrescentValue, // -1 to 1 (waxing positive, waning negative)
isWaxing, // boolean
isWaning, // boolean
} from 'celestial-theme-toggle';Phase Names
| Phase | Emoji | Age (days) | |-------|-------|------------| | New | 🌑 | 0 - 1.85 | | Waxing Crescent | 🌒 | 1.85 - 5.54 | | First Quarter | 🌓 | 5.54 - 9.23 | | Waxing Gibbous | 🌔 | 9.23 - 12.92 | | Full | 🌕 | 12.92 - 16.61 | | Waning Gibbous | 🌖 | 16.61 - 20.30 | | Last Quarter | 🌗 | 20.30 - 23.99 | | Waning Crescent | 🌘 | 23.99 - 27.68 |
CSS Custom Properties
Override these via the cssVariables option or in your own CSS:
:root {
/* Animation */
--celestial-transition: 800ms;
--celestial-ease: cubic-bezier(0.4, 0, 0.2, 1);
/* Transition progress (0-1, set by JS during animation) */
--celestial-transition-progress: 0;
/* Day sky */
--celestial-day-top: #4a90d9;
--celestial-day-mid: #87CEEB;
--celestial-day-bottom: #b8e4f0;
/* Night sky */
--celestial-night-top: #050a18;
--celestial-night-mid: #0B1026;
--celestial-night-bottom: #1a1a3e;
/* Sun */
--celestial-sun-core: #FFD700;
--celestial-sun-glow: #FFA500;
/* Moon */
--celestial-moon-face: #E8E4D9;
/* Toggle button */
--celestial-toggle-size: 56px;
}You can use --celestial-transition-progress in your own CSS to create custom transition effects:
.my-element {
opacity: calc(1 - var(--celestial-transition-progress, 0));
}HTML / CDN Usage
<script src="https://unpkg.com/celestial-theme-toggle/dist/index.umd.js"></script>
<script>
CelestialToggle.init({
theme: 'light',
fullPageSky: true,
transitionDuration: 1200,
});
</script>Migration from v1.x
What Changed
v1.x only animated the toggle icon and its own sky overlay. The rest of the application switched themes instantly.
v2.x introduces a cinematic transition engine that orchestrates a smooth transformation across the entire viewport.
Backward Compatibility
All existing APIs continue to work:
// v1.x — still works in v2.x
initCelestialToggle({ theme: 'light' });
instance.toggle();
instance.setTheme('dark');New Default Behavior
The toggle button now triggers a cinematic transition by default. If you want the old instant behavior:
initCelestialToggle({
animation: 'instant', // no transition, instant switch
});New Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| transitionDuration | number | 1000 | Animation duration in ms |
| delayThemeSwitch | boolean | true | Delay theme class switch |
| animateApplication | boolean | true | Overlay dimming effect |
| transitionRoot | string | 'html' | Target element for dimming |
| animation | string | 'sunset' | Animation style |
React Component
New props mirror the vanilla options:
<CelestialToggle
transitionDuration={1200}
delayThemeSwitch
animateApplication
transitionRoot="#app"
animation="sunset"
>
<App />
</CelestialToggle>License
MIT
