Repository navigation
Description
Note
Issue written by Claude
Summary
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):
useEffect(() => {
if (!themeCore?.url) {
setIsThemeCoreComplete(true);
return;
}
const themeCoreLink = document.createElement('link');
themeCoreLink.href = themeCore.url;
themeCoreLink.rel = 'stylesheet';
themeCoreLink.dataset.themeCore = 'true';
// ...
document.head.insertAdjacentElement('beforeend', themeCoreLink);
}, [themeCore?.url, onComplete]);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):
theme: {
core: { url: '.../core.min.css' },
variants: { light: { url: '.../light.min.css' } },
},Inspect <head>. You'll find:
..., <link data-theme-core>, <link data-theme-variant=\"light\">, <link data-theme-core>
The second data-theme-core link is the duplicate from the StrictMode re-mount, and it sits after the variant.
Observed impact — @edx/[email protected]
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.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
arbrandes commentedon May 12, 2026
Nice catch!
arbrandes commentedon May 12, 2026
Proposed fix: #262.
fix: dedupe theme core <link> in useThemeCore
openedx-semantic-release-bot commentedon May 14, 2026
🎉 This issue has been resolved in version 1.0.0-alpha.47 🎉
The release is available on:
Your semantic-release bot 📦🚀