Skip to content
Coastal Healthcare Advocates

Module: theme

Light/dark theme handling, the other Site preferences, plus two first-paint class flags.

Behavior

By default the page follows the device's light/dark setting (prefers-color-scheme). A visitor can override that with a theme toggle button. The override is remembered in localStorage together with the time it was made, and lasts 8 hours; after that the device setting applies again.

The chosen theme is applied as a data-theme="light|dark" attribute on <html>. The CSS custom properties (colors) and the CSS-swapped logo key off that attribute. With no attribute, the CSS falls back to the prefers-color-scheme media query.

Other Site preferences

Text size, animations and reader mode (set in the Site preferences dialog, prefs.js) are stored together under PREFS_KEY, with no expiry, and applied here before first paint as attributes on <html>:

  • data-text="lg|xl|xxl" scales the root font size (default: no attribute);
  • data-motion="off" acts like the OS reduced-motion setting (the build's scripts/postcss/motion-pref.js mirrors every reduced-motion block);
  • data-reader="on" switches on the plain reading layout.

window.chaPrefs exposes reading and changing all of it (theme included) to prefs.js, and fires cha:prefs on document after every change.

Why it loads the way it does

This file is a classic, render-blocking <script> in <head>, not an ES module and not deferred. It must run synchronously before the first paint so that:

  • a saved dark-theme choice applies immediately (no white flash), and
  • the js and hero-intro classes are on <html> before the body renders.

The toggle buttons live in the header/footer, which don't exist yet at that point, so their wiring waits for DOMContentLoaded.

Written in ES5 inside an IIFE so none of its variables leak globally.

Source:

Namespaces

chaPrefs

Members

(inner) KEY :string

localStorage key holding the visitor's SavedTheme.

Type:
  • string
Source:

(inner) PREFS_KEY :string

localStorage key holding the visitor's SavedPrefs.

Type:
  • string
Source:

(inner) PREF_VALUES :Object.<string, Array.<string>>

Allowed values for each stored preference; the first is the default, which is applied as "no attribute".

Type:
  • Object.<string, Array.<string>>
Source:

(inner) TTL :number

How long a toggled theme choice is honoured, in milliseconds (8 hours).

Type:
  • number
Source:

(inner) mq :MediaQueryList

Live media query for the device's dark-mode preference.

Type:
  • MediaQueryList
Source:

(inner) reduce :MediaQueryList

Live media query for the device's reduced-motion setting.

Type:
  • MediaQueryList
Source:

(inner) root :HTMLElement

The <html> element, which carries the data-theme attribute and the js / hero-intro / theme-transition classes.

Type:
  • HTMLElement
Source:

(inner) themeTimer :number|undefined

Timer id for removing the theme-transition class.

Type:
  • number | undefined
Source:

Methods

(inner) announce() → {void}

Tells listeners (the theme toggles, prefs.js) that a preference changed.

Source:
Returns:
Type
void

(inner) apply(choice) → {void}

Applies a theme to the page by setting or removing data-theme on <html>.

Parameters:
Name Type Description
choice 'light' | 'dark' | null

An explicit theme, or null to remove the attribute and let the CSS follow the operating-system setting.

Source:
Returns:
Type
void

(inner) applyPrefs(prefs) → {void}

Applies preferences to <html>: a default removes its attribute.

Parameters:
Name Type Description
prefs SavedPrefs

The full preference set.

Source:
Returns:
Type
void

(inner) crossfade(change) → {void}

Makes a theme switch fade gently, like dusk falling, instead of snapping. Only ever called on an explicit toggle — never on page load, where a fade would look like a glitch.

Where the browser has View Transitions, change runs inside one: the browser cross-fades a picture of the old page into the new one on the compositor, so the page restyles once. Elsewhere the theme-transition class gives every element its own color transition for 320 ms (the CSS is in src/input.css), and every frame of that fade restyles the whole page. Measured 2026-09-18 with the CPU slowed 4×: the class took 384 ms to answer the press and its next three frames ran 120–370 ms each; the view transition answers in 40 ms, then spends one 67 ms frame on the restyle and fades at full rate. So the class is only the fallback. Under reduced motion, or with Animations off, the theme simply swaps.

Parameters:
Name Type Description
change function

Switches the theme and tells listeners.

Source:
Returns:
Type
void

(inner) effective() → {'light'|'dark'}

Works out which theme is actually showing right now: the saved choice if there is one, otherwise whatever the device prefers.

Source:
Returns:

The theme currently in effect.

Type
'light' | 'dark'

(inner) readPrefs() → {SavedPrefs}

Reads the saved preferences, dropping anything unrecognised.

Source:
Returns:

Every preference, defaults filled in.

Type
SavedPrefs

(inner) ready(fn) → {void}

Runs fn once the DOM is parsed — immediately if that's already happened, otherwise on DOMContentLoaded.

Parameters:
Name Type Description
fn function

Callback to run.

Source:
Returns:
Type
void

(inner) save(theme) → {void}

Saves the visitor's theme choice with the current time. Silently does nothing if storage is unavailable.

Parameters:
Name Type Description
theme 'light' | 'dark'

The theme to remember.

Source:
Returns:
Type
void

(inner) stored() → {'light'|'dark'|null}

Returns the visitor's theme choice if it was made within the last TTL milliseconds.

Anything else — an expired choice, a record from the future (clock changes), an unreadable value, or the bare 'light' / 'dark' string that earlier versions of the site stored — is deleted so it can't linger.

Source:
Returns:

The still-valid choice, or null.

Type
'light' | 'dark' | null

(inner) sync() → {void}

Updates every toggle button to match the current theme: its accessible name (aria-label) and tooltip (title), its pressed state (aria-pressed, which the CSS also uses for the switch's look), and its visible label text.

Source:
Returns:
Type
void

Type Definitions

SavedPrefs

The value stored in localStorage under PREFS_KEY. Missing keys mean the default.

Type:
  • Object
Properties:
Name Type Attributes Description
text 'md' | 'lg' | 'xl' | 'xxl' <optional>

Text size step.

motion 'on' | 'off' <optional>

Animations.

reader 'off' | 'on' <optional>

Reader mode.

Source:

SavedTheme

The value stored in localStorage under KEY.

Type:
  • Object
Properties:
Name Type Description
theme 'light' | 'dark'

The theme the visitor picked.

at number

When they picked it (Date.now() ms).

Source: