Sunrise & Sunset
@theme-kit/core · every framework
Follow the sun: Theme Kit switches the app automatically at sunrise and sunset. No configuration required — lightTheme / darkTheme adapt to whichever theme family the visitor has selected (falling back to Theme Kit's neutral themes), and coordinates are auto-detected from each visitor's browser timezone — so the schedule works for every user anywhere in the world. Pass any of them explicitly when you want to pin a choice. The engine lives entirely in @theme-kit/core (NOAA-style solar math); every framework exposes it through a native reactive accessor — useThemeSchedule() in React / Next / Vue / Nuxt / Solid, getThemeSchedule() in Svelte, and injectThemeSchedule() in Angular.
1How it works
calculateSunTimes(date, lat, lon) computes today's sunrise and sunset using the NOAA solar algorithm (zenith-based, corrected for the equation of time). At runtime the schedule checks the clock every checkInterval and applies lightTheme during the day or darkTheme at night.
No themes required either. lightTheme and darkTheme are optional. When omitted, the schedule derives them from the currently selected theme's family — pick plum-dark in your switcher and the schedule uses plum-light / plum-dark — and falls back to Theme Kit's neutral light / dark themes when the current theme has no family counterpart. The resolved pair re-derives automatically whenever the user switches family, and is reported in state as lightTheme / darkTheme.
No coordinates required. When you omit latitude and longitude, Theme Kit resolves the location from the visitor's IANA timezone (via Intl.DateTimeFormat().resolvedOptions().timeZone) — each zone is anchored to the reference city the tz database uses, which is more than accurate enough for day/night switching. Pin a timezone explicitly with timeZone (it can be changed at runtime with schedule.set()), or set autoDetectLocation: false to force the default coordinates.
SSR boundary. The engine attaches its timer and DOM apply logic only on the client (typeof window !== "undefined"), and auto-detection runs only on the client too — so server renders never leak timers and stay deterministic. Your SSR framework resolves the initial theme as usual (zero flash of the wrong theme); the client-side schedule takes over activation from there. Configure the schedule on the server provider (Next.js / Nuxt) and the same settings apply to the hydrated client runtime.
Because detection happens per visitor, the schedule is correct anywhere in the world: a user in Kathmandu gets Kathmandu's sunrise/sunset, a user in Helsinki gets Helsinki's — with the same scheduled block and no network call.
2Setup
1import { ThemeProvider } from "@theme-kit/react";
2
3export function App() {
4 return (
5 <ThemeProvider
6 themes={themes}
7 defaultTheme="mint-light"
8 scheduled={{
9 // Everything is optional. lightTheme/darkTheme adapt to the current
10 // theme family (fallback: neutral light/dark), and coordinates are
11 // auto-detected from the visitor's timezone — so the schedule is
12 // correct for every user anywhere, with no config at all.
13 // lightTheme: "mint-light",
14 // darkTheme: "mint-dark",
15 // timeZone: "Asia/Kathmandu",
16 }}
17 >
18 <ThemeSwitcher />
19 </ThemeProvider>
20 );
21}3Read & control the schedule
1import { useThemeSchedule } from "@theme-kit/react";
2
3export function ScheduleToggle() {
4 const schedule = useThemeSchedule();
5 const state = schedule?.state;
6
7 return (
8 <div>
9 <button
10 onClick={() =>
11 state?.enabled ? schedule?.disable() : schedule?.enable()
12 }
13 >
14 {state?.enabled ? "Disable" : "Enable"} schedule
15 </button>
16 <p>Status: {state?.status}</p>
17 <p>
18 {state?.timeZone} ({state?.latitude?.toFixed(2)},{" "}
19 {state?.longitude?.toFixed(2)}) — Sunrise{" "}
20 {state?.sunrise?.toLocaleTimeString()} · Sunset{" "}
21 {state?.sunset?.toLocaleTimeString()} · Next{" "}
22 {state?.nextTransition?.theme} at{" "}
23 {state?.nextTransition?.at.toLocaleTimeString()}
24 </p>
25 <select
26 value={state?.timeZone ?? ""}
27 onChange={(e) => schedule?.set({ timeZone: e.target.value })}
28 >
29 <option value="">Auto (my location)</option>
30 {timeZones.map((zone) => (
31 <option key={zone} value={zone}>{zone}</option>
32 ))}
33 </select>
34 </div>
35 );
36}enabled — schedule is on. active — enabled and the applied theme is one of the scheduled light/dark themes. status — "active" or "disabled". timeZone — the resolved timezone (or null when explicit coordinates are used). latitude / longitude — the resolved coordinates. autoDetected — coordinates came from a timezone rather than explicit values. lightTheme / darkTheme — the resolved scheduled pair (auto-derived from the current theme family when not configured). nextTransition — { at, theme, type } where type is "activation" (sunrise) or "deactivation" (sunset).4Options
| Option | Type | Description |
|---|---|---|
| lightTheme | string (optional) | Theme name applied between sunrise and sunset. Optional — when omitted the schedule derives it from the currently selected theme's family (e.g. `plum-dark` → `plum-light`), falling back to the built-in neutral `light` theme. |
| darkTheme | string (optional) | Theme name applied between sunset and sunrise. Optional — same derivation as `lightTheme`, falling back to the built-in neutral `dark` theme. |
| latitude / longitude | number (optional) | Coordinates used for the solar calculation. Optional — when omitted the location is resolved from `timeZone` or each visitor's browser timezone, so the schedule is correct anywhere in the world without configuration. |
| timeZone | string (optional) | IANA timezone to resolve coordinates from when latitude/longitude are omitted (e.g. "Asia/Kathmandu"). Takes precedence over auto-detection. Changeable at runtime via schedule.set(). |
| autoDetectLocation | boolean (optional) | Auto-detect the visitor's location from their browser timezone when no coordinates/timezone are given. Default true. Set false to force the default coordinates and keep SSR fully deterministic. |
| checkInterval | number (ms) | How often the schedule re-checks solar time. Default 60000 (1 minute). |
| skipApplyMs | number (ms) | Ignore schedule-driven applies within this window after a manual selection (e.g. a cross-tab sync). Default 0. |
| enabled | boolean | Start the schedule on. Default true. |
| set({ … }) | method | Reposition or reconfigure at runtime — latitude, longitude, timeZone, autoDetectLocation, checkInterval, skipApplyMs or enabled. Pass { autoDetectLocation: true } to clear explicit settings and return to per-visitor detection. |
5Schedule vs. manual override
enable()applies the correct light/dark theme immediately, then re-checks everycheckInterval.- A manual pick of any other theme is honored until the next check, at which point the schedule re-applies its light/dark selection. While overridden,
activeisfalse. skipApplyMswidens the window after a manual selection (or cross-tab sync) before the schedule re-asserts control.disable()leaves the current theme untouched — the schedule simply stops re-applying.- If a configured
lightThemeordarkThemedoesn't exist in the theme registry, the schedule stays off (status"disabled").