Sunrise & Sunset
@themvora/core · every framework
Follow the sun: Themvora 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 Themvora'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 @themvora/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.
1Setup
import { ThemeProvider } from "@themvora/react";
import { themes } from "./themes";
export function App() {
return (
<ThemeProvider
themes={themes}
defaultTheme="mint-light"
scheduled={{
enabled: true,
// Omit latitude/longitude to resolve the visitor's own location
// from their timezone.
}}
>
<YourApp />
</ThemeProvider>
);
}2Read & control the schedule
import { useThemeSchedule } from "@themvora/react";
// Any IANA zone works — this short list is only for the picker below.
const TIME_ZONES = ["", "Asia/Kathmandu", "Europe/Helsinki", "America/New_York"];
export function ScheduleToggle() {
const schedule = useThemeSchedule();
const state = schedule?.state;
return (
<div>
<button
onClick={() =>
state?.enabled ? schedule?.disable() : schedule?.enable()
}
>
{state?.enabled ? "Disable" : "Enable"} schedule
</button>
<p>Status: {state?.status}</p>
<p>
{state?.timeZone} ({state?.latitude?.toFixed(2)},{" "}
{state?.longitude?.toFixed(2)}) — Sunrise{" "}
{state?.sunrise?.toLocaleTimeString()} · Sunset{" "}
{state?.sunset?.toLocaleTimeString()} · Next{" "}
{state?.nextTransition?.theme} at{" "}
{state?.nextTransition?.at.toLocaleTimeString()}
</p>
<select
value={state?.timeZone ?? ""}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => schedule?.set({ timeZone: e.target.value })}
>
<option value="">Auto (my location)</option>
{TIME_ZONES.filter(Boolean).map((zone) => (
<option key={zone} value={zone}>{zone}</option>
))}
</select>
</div>
);
}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).3Options
| 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. |
scheduled prop is reconciled in place — changing it after mount installs, repositions or replaces the schedule without recreating the runtime or resetting the active theme and history. Pass scheduled={false} to remove it: runtime.schedule becomes null and its timers and listeners are disposed. An unusable option (a non-positive checkInterval, an out-of-range coordinate) is reported as TV_CONFIG_OPTION_INVALID, and the previous schedule is kept.4Schedule 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").
5How 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 Themvora'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, Themvora 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.