Color-mode boot script is emitted after the store and composable bundles, so useColorMode() in a store silently gets default config
Verified against the stx fork at main (0dcb6fbd0e), built from source, in a
real app on Chromium via CDP.
Symptom
A store that calls useColorMode() writes to the wrong storage key and never
sets the configured attribute. With this in stx.config.ts:
app: {
colorMode: {
storageKey: 'bughq_theme',
attribute: 'data-theme',
darkClass: null,
initialMode: 'auto',
},
}and a store:
export const useTheme = defineStore('theme', () => {
const cm = useColorMode() // no options — by design, config is central
function toggle() { cm.set(cm.mode === 'dark' ? 'light' : 'dark') }
return { cm, toggle }
})calling toggle() produces:
document.documentElement.getAttribute('data-theme') → unchanged
localStorage.getItem('bughq_theme') → null
localStorage.getItem('stx-color-mode') → 'light' ← default keyThe theme appears to do nothing, and the user's stored preference is written somewhere the boot script will never read on the next load.
Cause
useColorMode reads its configuration from window.__STX_COLOR_MODE__, which the
pre-paint boot script publishes. But the boot script is emitted after the store
and composable bundles. Byte offsets within <head> of a rendered page:
byte 2260 <script data-stx-runtime src="/_stx/runtime.js"> (external, render-blocking)
byte 2318 <script data-stx-stores> <-- calls useColorMode()
byte 3656 <script data-stx-composables>
byte 5285 <script data-stx-color-mode-boot> <-- publishes __STX_COLOR_MODE__
byte 6344 <link rel="stylesheet" …>So at the moment the store runs, window.__STX_COLOR_MODE__ is undefined, and
var storageKey = pick(options.storageKey, boot && boot.storageKey, 'stx-color-mode');
var attribute = pick(options.attribute, boot && boot.attribute, null);falls through to the defaults — 'stx-color-mode' and attribute: null. applyDOM
then skips the attribute entirely because attribute is null.
window.__STX_COLOR_MODE__ does exist by the time you inspect it from the console,
which makes this look like it should work. It is purely an ordering problem.
This contradicts the module's own contract
color-mode-boot.d.ts states:
Insert the boot script as the first thing in
<head>. Placement matters twice over: the script must run before the browser paints, and a stylesheet<link>above it would block its execution (CSS blocks JS), delaying the very thing it exists to make early.
It is not first. It is fifth.
Second, related problem: a render-blocking script sits ahead of it
<script data-stx-runtime src="/_stx/runtime.js"> has no async/defer, so the
parser must download and execute the whole runtime bundle before reaching the
boot script. That is a larger delay than the stylesheet case the doc already guards
against, and it defeats the "run before paint" purpose even for apps that use no
stores at all.
Suggested fix
Emit the color-mode boot script first in <head>, ahead of the runtime, stores and
composables bundles — which is what injectColorModeBootScript already documents.
That fixes both problems at once: stores see the published config, and the theme
lands before any bundle has to download.
Workaround for anyone hitting this now
Pass the options explicitly at the call site instead of relying on the published config:
const cm = useColorMode({ storageKey: 'bughq_theme', attribute: 'data-theme', darkClass: null })This duplicates the configuration, which is exactly what app.colorMode exists to
avoid — but it is correct regardless of script order.