This document traces how theming works in the Callora Frontend, from the pre-paint inline script in index.html through to runtime theme transitions. Theme handling is split across four pieces that must stay in agreement:
idxex.html— the pre-paint inline script that applies the theme before first paint.src/ThemeContext.ts— the Rune context that owns the current theme and persists changes.src/ThemeToggle.tsx— the UI control that cycles between light and dark.src/styles/theme-transition.css— the CSS that gates color transitions until the app is ready.
- The browser parses
index.html. The inline script in the<head>runs before the body is painted. It reads the stored theme fromlocalStorageunder the keycallora-theme, resolvessystemagainstwindow.matchMedia('(prefers-color-scheme: dark)'), and setsdata-themeon<html>. This prevents a flash of the wrong theme because the correct theme is already on the root element before any pixel is drawn. - The main bundle loads and
ThemeProvidermounts.ThemeContextinitializes its state from the same sources the inline script used: thecallora-themelocalStorage key and the system color scheme media query. It also attaches amatchMedialistener so that when the user hassystemselected, the effective theme follows the operating system in real time. ThemeContextwritesdata-themeondocument.documentElementwhenever the resolved theme changes and persists the selection tolocalStorage.ThemeTogglecalls into the context to change the theme. The context adds thetheme-transitions-readyclass to<html>on a later tick so the initial paint is not animated.src/styles/theme-transition.cssonly enables color transitions whentheme-transitions-readyis present on<html>. Theno-theme-transitionclass is an escape hatch that suppresses transitions for a specific subtree or element.
| Item | Value |
|---|---|
| Storage key | callora-theme |
| Allowed values | light, dark, system |
| Default when nothing is stored | dark |
| Resolved theme attribute | data-theme="light" or data-theme="dark" on <html> |
light and dark are the effective themes. system is a preference that resolves to light or dark based on prefers-color-scheme. The inline script and ThemeContext must agree on this key and these values; changing one without the other will regress to a flash of the wrong theme.
Theme transitions are disabled by default and only become active once the app has mounted and applied the initial theme. This is controlled by the theme-transitions-ready class on <html>.
ThemeContextaddstheme-transitions-readyafter the first render (e.g. in arequestAnimationFrameoruseEffecttick).src/styles/theme-transition.cssscopes its color transition rules underhtml.theme-transitions-ready.- Because the class is added after the initial paint, loading the app never animates from the default to the stored theme.
Add the no-theme-transition class to an element or subtree to opt it out of theme color transitions. This is useful for elements that must snap to the new theme immediately, such as media, canvases, or components that manage their own animations.
/* Example: this element will not animate when the theme changes. */
.no-theme-transition,
.no-theme-transition * {
transition: none !important;
}- [] Use CSS variables (e.g.
--color-background,--color-text) rather than hard-coded colors. - [] Verify the component renders correctly in both
data-theme="light"anddata-theme="dark"`. - [] If the component must not animate during theme changes, add the
no-theme-transitionclass. - [] Do not read or write
data-themeor thecallora-themelocalStorage key directly; useThemeContext. - [] If the component needs the resolved theme, consume it from
ThemeContextsosystemis resolved consistently. - [] Test the component with the existing theme tests (
npm test -- --run src/ThemeContext.test.tsx src/theme-transition.test.tsx).