You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
useThemeCore appends a new <link> to document.head on every effect run without deduplicating against an existing one. Under React StrictMode (which is enabled in the dev shell), the effect fires twice on mount, leaving a duplicate core stylesheet appended after the theme variant stylesheet. Any :root declaration that the core and variant share gets resolved to the core's value rather than the variant's.
Where
runtime/react/hooks/theme/useThemeCore.js (built output below for reference):
No document.head.querySelector('link[data-theme-core=\"true\"]') check, and no cleanup. Compare with useThemeVariants, which dedups via document.head.querySelector(\link[href='${url}']`)` and updates the existing element instead of appending a new one.
Repro
Configure a site with a properly split core + light theme (core.min.css carrying base tokens, light.min.css carrying variant overrides). In dev (StrictMode on):
The second data-theme-core link is the duplicate from the StrictMode re-mount, and it sits after the variant.
Observed impact — @edx/elm-theme@1.11.1
Using the elm theme as a concrete example: core.min.css and light.min.css both write to :root, and three variables overlap. After this bug, the trailing core link wins for all three:
variable
core
light (intended)
resolved with bug
--pgn-size-input-btn-border-width
1px
var(--pgn-size-border-width) → 1px
equivalent
--pgn-spacing-btn-focus-gap
2px
var(--pgn-size-btn-focus-width) → 2px
equivalent
--pgn-typography-btn-font-weight
500
var(--pgn-typography-font-weight-normal) → 400
500 instead of 400
So with elm theme today, every button renders one weight heavier than the variant intends. Other theme packages that split tokens between core and a variant the same way will hit analogous problems on whatever variables they happen to share.
Suggested fix
Mirror what useThemeVariants already does — either:
Dedup against an existing link[data-theme-core=\"true\"] and update it in place instead of appending a new one, or
Return a cleanup function from the effect that removes the link, so StrictMode's discard-and-remount cycle nets to zero extra links.
(1) keeps behavior identical between dev and prod and avoids a flash where the core is briefly removed.
Note
Issue written by Claude
Summary
useThemeCoreappends a new<link>todocument.headon every effect run without deduplicating against an existing one. Under ReactStrictMode(which is enabled in the dev shell), the effect fires twice on mount, leaving a duplicate core stylesheet appended after the theme variant stylesheet. Any:rootdeclaration that the core and variant share gets resolved to the core's value rather than the variant's.Where
runtime/react/hooks/theme/useThemeCore.js(built output below for reference):No
document.head.querySelector('link[data-theme-core=\"true\"]')check, and no cleanup. Compare withuseThemeVariants, which dedups viadocument.head.querySelector(\link[href='${url}']`)` and updates the existing element instead of appending a new one.Repro
Configure a site with a properly split core + light theme (
core.min.csscarrying base tokens,light.min.csscarrying variant overrides). In dev (StrictMode on):Inspect
<head>. You'll find:The second
data-theme-corelink is the duplicate from the StrictMode re-mount, and it sits after the variant.Observed impact —
@edx/elm-theme@1.11.1Using the elm theme as a concrete example:
core.min.cssandlight.min.cssboth write to:root, and three variables overlap. After this bug, the trailing core link wins for all three:--pgn-size-input-btn-border-width1pxvar(--pgn-size-border-width)→1px--pgn-spacing-btn-focus-gap2pxvar(--pgn-size-btn-focus-width)→2px--pgn-typography-btn-font-weight500var(--pgn-typography-font-weight-normal)→400500instead of400So with elm theme today, every button renders one weight heavier than the variant intends. Other theme packages that split tokens between
coreand a variant the same way will hit analogous problems on whatever variables they happen to share.Suggested fix
Mirror what
useThemeVariantsalready does — either:link[data-theme-core=\"true\"]and update it in place instead of appending a new one, or(1) keeps behavior identical between dev and prod and avoids a flash where the core is briefly removed.