I’ll build this as a standalone advanced theme-mode chunk for RX Theme: dark/light/system mode, persistence, early DOM sync, accessible controls, events, and WordPress-friendly safety guards.
Yes friend — create this file:
assets/static-js/static-chunks-js/chunk-033-theme-mode.js
Paste this full advanced code inside it.
/*!
* RX Theme - Theme Mode Chunk
* File: assets/static-js/static-chunks-js/chunk-033-theme-mode.js
* Purpose: Advanced Light / Dark / System theme mode controller
* Author: RX Theme
*/
(function () {
'use strict';
/**
* RX Theme Mode System
*
* Features:
* - Light mode
* - Dark mode
* - System mode
* - LocalStorage persistence
* - Cookie fallback
* - HTML data attributes
* - Body class support
* - Meta theme-color update
* - Accessibility support
* - Button / select / checkbox control support
* - Keyboard support
* - Custom events for other scripts
* - WordPress admin-bar safe
* - No jQuery dependency
*/
var RXThemeMode = {
version: '1.0.0',
config: {
storageKey: 'rx_theme_mode',
cookieName: 'rx_theme_mode',
defaultMode: 'system',
modes: ['light', 'dark', 'system'],
htmlAttribute: 'data-rx-theme',
htmlResolvedAttribute: 'data-rx-theme-resolved',
htmlModeAttribute: 'data-rx-theme-mode',
bodyLightClass: 'rx-theme-light',
bodyDarkClass: 'rx-theme-dark',
bodySystemClass: 'rx-theme-system',
transitionClass: 'rx-theme-changing',
readyClass: 'rx-theme-mode-ready',
controlSelector: '[data-rx-theme-toggle], [data-rx-theme-mode-control], [data-theme-toggle], [data-theme-mode]',
selectSelector: 'select[data-rx-theme-select], select[data-theme-select]',
checkboxSelector: 'input[type="checkbox"][data-rx-theme-checkbox], input[type="checkbox"][data-theme-checkbox]',
metaThemeColorLight: '#ffffff',
metaThemeColorDark: '#0f172a',
transitionDuration: 250,
cookieDays: 365,
debug: false
},
state: {
currentMode: null,
resolvedMode: null,
systemDark: false,
initialized: false,
mediaQuery: null
},
/**
* Initialize
*/
init: function () {
if (this.state.initialized) {
return;
}
this.state.mediaQuery = this.getSystemDarkQuery();
this.state.systemDark = this.detectSystemDark();
var savedMode = this.getSavedMode();
var initialMode = this.normalizeMode(savedMode || this.config.defaultMode);
this.applyMode(initialMode, {
save: false,
dispatch: false,
transition: false
});
this.bindSystemListener();
this.bindControls();
this.bindKeyboardShortcut();
this.observeDynamicControls();
this.markReady();
this.state.initialized = true;
this.dispatchEvent('rxThemeModeReady', {
mode: this.state.currentMode,
resolvedMode: this.state.resolvedMode,
systemDark: this.state.systemDark
});
this.log('RX Theme Mode initialized:', this.state);
},
/**
* Get system prefers-color-scheme query
*/
getSystemDarkQuery: function () {
if (typeof window.matchMedia !== 'function') {
return null;
}
try {
return window.matchMedia('(prefers-color-scheme: dark)');
} catch (error) {
return null;
}
},
/**
* Detect system dark mode
*/
detectSystemDark: function () {
var query = this.getSystemDarkQuery();
if (!query) {
return false;
}
return !!query.matches;
},
/**
* Normalize mode
*/
normalizeMode: function (mode) {
mode = String(mode || '').toLowerCase().trim();
if (this.config.modes.indexOf(mode) === -1) {
return this.config.defaultMode;
}
return mode;
},
/**
* Resolve actual visual mode
*/
resolveMode: function (mode) {
mode = this.normalizeMode(mode);
if (mode === 'system') {
return this.detectSystemDark() ? 'dark' : 'light';
}
return mode;
},
/**
* Apply selected mode
*/
applyMode: function (mode, options) {
options = options || {};
var normalizedMode = this.normalizeMode(mode);
var resolvedMode = this.resolveMode(normalizedMode);
var previousMode = this.state.currentMode;
var previousResolvedMode = this.state.resolvedMode;
if (options.transition !== false) {
this.enableTransition();
}
this.state.currentMode = normalizedMode;
this.state.resolvedMode = resolvedMode;
this.state.systemDark = this.detectSystemDark();
this.updateHtmlAttributes(normalizedMode, resolvedMode);
this.updateBodyClasses(normalizedMode, resolvedMode);
this.updateMetaThemeColor(resolvedMode);
this.updateControls(normalizedMode, resolvedMode);
if (options.save !== false) {
this.saveMode(normalizedMode);
}
if (options.dispatch !== false) {
this.dispatchEvent('rxThemeModeChange', {
mode: normalizedMode,
resolvedMode: resolvedMode,
previousMode: previousMode,
previousResolvedMode: previousResolvedMode,
systemDark: this.state.systemDark
});
}
this.log('Theme mode applied:', normalizedMode, resolvedMode);
},
/**
* Set light mode
*/
setLight: function () {
this.applyMode('light');
},
/**
* Set dark mode
*/
setDark: function () {
this.applyMode('dark');
},
/**
* Set system mode
*/
setSystem: function () {
this.applyMode('system');
},
/**
* Toggle light/dark only
*/
toggle: function () {
var resolved = this.state.resolvedMode || this.resolveMode(this.state.currentMode);
if (resolved === 'dark') {
this.applyMode('light');
} else {
this.applyMode('dark');
}
},
/**
* Cycle light -> dark -> system
*/
cycle: function () {
var current = this.state.currentMode || this.config.defaultMode;
var next = 'light';
if (current === 'light') {
next = 'dark';
} else if (current === 'dark') {
next = 'system';
} else {
next = 'light';
}
this.applyMode(next);
},
/**
* Update HTML attributes
*/
updateHtmlAttributes: function (mode, resolvedMode) {
var html = document.documentElement;
html.setAttribute(this.config.htmlAttribute, resolvedMode);
html.setAttribute(this.config.htmlResolvedAttribute, resolvedMode);
html.setAttribute(this.config.htmlModeAttribute, mode);
html.style.colorScheme = resolvedMode;
},
/**
* Update body classes
*/
updateBodyClasses: function (mode, resolvedMode) {
if (!document.body) {
return;
}
var body = document.body;
body.classList.remove(
this.config.bodyLightClass,
this.config.bodyDarkClass,
this.config.bodySystemClass
);
if (resolvedMode === 'dark') {
body.classList.add(this.config.bodyDarkClass);
} else {
body.classList.add(this.config.bodyLightClass);
}
if (mode === 'system') {
body.classList.add(this.config.bodySystemClass);
}
},
/**
* Add short transition class
*/
enableTransition: function () {
var html = document.documentElement;
var transitionClass = this.config.transitionClass;
var duration = this.config.transitionDuration;
html.classList.add(transitionClass);
window.clearTimeout(this._transitionTimer);
this._transitionTimer = window.setTimeout(function () {
html.classList.remove(transitionClass);
}, duration);
},
/**
* Mark theme mode ready
*/
markReady: function () {
document.documentElement.classList.add(this.config.readyClass);
if (document.body) {
document.body.classList.add(this.config.readyClass);
}
},
/**
* Update browser theme color
*/
updateMetaThemeColor: function (resolvedMode) {
var color = resolvedMode === 'dark'
? this.config.metaThemeColorDark
: this.config.metaThemeColorLight;
var meta = document.querySelector('meta[name="theme-color"]');
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute('name', 'theme-color');
document.head.appendChild(meta);
}
meta.setAttribute('content', color);
},
/**
* Save mode to localStorage and cookie
*/
saveMode: function (mode) {
mode = this.normalizeMode(mode);
try {
window.localStorage.setItem(this.config.storageKey, mode);
} catch (error) {
this.log('localStorage unavailable');
}
this.setCookie(this.config.cookieName, mode, this.config.cookieDays);
},
/**
* Get saved mode
*/
getSavedMode: function () {
var mode = null;
try {
mode = window.localStorage.getItem(this.config.storageKey);
} catch (error) {
mode = null;
}
if (!mode) {
mode = this.getCookie(this.config.cookieName);
}
return this.normalizeMode(mode || this.config.defaultMode);
},
/**
* Set cookie
*/
setCookie: function (name, value, days) {
try {
var expires = '';
var date;
if (days) {
date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
expires = '; expires=' + date.toUTCString();
}
document.cookie =
encodeURIComponent(name) +
'=' +
encodeURIComponent(value) +
expires +
'; path=/; SameSite=Lax';
} catch (error) {
this.log('Cookie save failed');
}
},
/**
* Get cookie
*/
getCookie: function (name) {
try {
var nameEQ = encodeURIComponent(name) + '=';
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
if (cookie.indexOf(nameEQ) === 0) {
return decodeURIComponent(cookie.substring(nameEQ.length));
}
}
} catch (error) {
return null;
}
return null;
},
/**
* Listen to system color scheme change
*/
bindSystemListener: function () {
var self = this;
var query = this.state.mediaQuery;
if (!query) {
return;
}
var handler = function (event) {
self.state.systemDark = !!event.matches;
if (self.state.currentMode === 'system') {
self.applyMode('system', {
save: false,
dispatch: true,
transition: true
});
}
self.dispatchEvent('rxThemeSystemModeChange', {
systemDark: self.state.systemDark,
mode: self.state.currentMode,
resolvedMode: self.state.resolvedMode
});
};
if (typeof query.addEventListener === 'function') {
query.addEventListener('change', handler);
} else if (typeof query.addListener === 'function') {
query.addListener(handler);
}
},
/**
* Bind all theme controls
*/
bindControls: function (root) {
root = root || document;
this.bindButtonControls(root);
this.bindSelectControls(root);
this.bindCheckboxControls(root);
},
/**
* Button controls
*
* Supported:
* <button data-rx-theme-toggle>Toggle</button>
* <button data-rx-theme-toggle="cycle">Cycle</button>
* <button data-rx-theme-mode-control="dark">Dark</button>
* <button data-theme-mode="light">Light</button>
*/
bindButtonControls: function (root) {
var self = this;
var controls = root.querySelectorAll(this.config.controlSelector);
Array.prototype.forEach.call(controls, function (control) {
if (control.__rxThemeModeBound) {
return;
}
control.__rxThemeModeBound = true;
control.addEventListener('click', function (event) {
var requestedMode =
control.getAttribute('data-rx-theme-mode-control') ||
control.getAttribute('data-theme-mode');
var toggleType =
control.getAttribute('data-rx-theme-toggle') ||
control.getAttribute('data-theme-toggle');
if (requestedMode) {
self.applyMode(requestedMode);
} else if (toggleType === 'cycle') {
self.cycle();
} else {
self.toggle();
}
event.preventDefault();
});
});
},
/**
* Select controls
*
* Supported:
* <select data-rx-theme-select>
* <option value="light">Light</option>
* <option value="dark">Dark</option>
* <option value="system">System</option>
* </select>
*/
bindSelectControls: function (root) {
var self = this;
var selects = root.querySelectorAll(this.config.selectSelector);
Array.prototype.forEach.call(selects, function (select) {
if (select.__rxThemeModeBound) {
return;
}
select.__rxThemeModeBound = true;
select.addEventListener('change', function () {
self.applyMode(select.value);
});
});
},
/**
* Checkbox controls
*
* Supported:
* <input type="checkbox" data-rx-theme-checkbox>
*/
bindCheckboxControls: function (root) {
var self = this;
var checkboxes = root.querySelectorAll(this.config.checkboxSelector);
Array.prototype.forEach.call(checkboxes, function (checkbox) {
if (checkbox.__rxThemeModeBound) {
return;
}
checkbox.__rxThemeModeBound = true;
checkbox.addEventListener('change', function () {
if (checkbox.checked) {
self.applyMode('dark');
} else {
self.applyMode('light');
}
});
});
},
/**
* Update control states
*/
updateControls: function (mode, resolvedMode) {
var allControls = document.querySelectorAll(this.config.controlSelector);
var selects = document.querySelectorAll(this.config.selectSelector);
var checkboxes = document.querySelectorAll(this.config.checkboxSelector);
Array.prototype.forEach.call(allControls, function (control) {
control.setAttribute('aria-pressed', resolvedMode === 'dark' ? 'true' : 'false');
control.setAttribute('data-rx-theme-current', mode);
control.setAttribute('data-rx-theme-resolved-current', resolvedMode);
var label = 'Switch theme mode. Current mode is ' + mode + '. Visual mode is ' + resolvedMode + '.';
control.setAttribute('aria-label', label);
});
Array.prototype.forEach.call(selects, function (select) {
select.value = mode;
select.setAttribute('data-rx-theme-current', mode);
});
Array.prototype.forEach.call(checkboxes, function (checkbox) {
checkbox.checked = resolvedMode === 'dark';
checkbox.setAttribute('aria-checked', resolvedMode === 'dark' ? 'true' : 'false');
});
},
/**
* Observe dynamically added buttons/selects
*/
observeDynamicControls: function () {
var self = this;
if (typeof MutationObserver !== 'function') {
return;
}
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
Array.prototype.forEach.call(mutation.addedNodes, function (node) {
if (!node || node.nodeType !== 1) {
return;
}
if (
node.matches &&
(
node.matches(self.config.controlSelector) ||
node.matches(self.config.selectSelector) ||
node.matches(self.config.checkboxSelector)
)
) {
self.bindControls(node.parentNode || document);
self.updateControls(self.state.currentMode, self.state.resolvedMode);
return;
}
if (node.querySelector) {
self.bindControls(node);
self.updateControls(self.state.currentMode, self.state.resolvedMode);
}
});
});
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
this._observer = observer;
},
/**
* Optional keyboard shortcut
* Ctrl + Alt + D = toggle dark/light
*/
bindKeyboardShortcut: function () {
var self = this;
document.addEventListener('keydown', function (event) {
var key = String(event.key || '').toLowerCase();
if (event.ctrlKey && event.altKey && key === 'd') {
self.toggle();
event.preventDefault();
}
});
},
/**
* Dispatch custom event
*/
dispatchEvent: function (name, detail) {
var event;
try {
event = new CustomEvent(name, {
detail: detail,
bubbles: true
});
} catch (error) {
event = document.createEvent('CustomEvent');
event.initCustomEvent(name, true, false, detail);
}
document.dispatchEvent(event);
},
/**
* Public getter
*/
getMode: function () {
return {
mode: this.state.currentMode,
resolvedMode: this.state.resolvedMode,
systemDark: this.state.systemDark
};
},
/**
* Reset saved preference
*/
reset: function () {
try {
window.localStorage.removeItem(this.config.storageKey);
} catch (error) {}
this.setCookie(this.config.cookieName, '', -1);
this.applyMode(this.config.defaultMode);
},
/**
* Debug logger
*/
log: function () {
if (!this.config.debug || !window.console) {
return;
}
window.console.log.apply(window.console, arguments);
}
};
/**
* Early mode apply before DOM ready
* This reduces dark/light flashing.
*/
(function earlyApply() {
var storageKey = RXThemeMode.config.storageKey;
var defaultMode = RXThemeMode.config.defaultMode;
var mode = defaultMode;
var resolvedMode = 'light';
var systemDark = false;
try {
mode = window.localStorage.getItem(storageKey) || defaultMode;
} catch (error) {
mode = defaultMode;
}
mode = RXThemeMode.normalizeMode(mode);
try {
systemDark = window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches;
} catch (error) {
systemDark = false;
}
if (mode === 'system') {
resolvedMode = systemDark ? 'dark' : 'light';
} else {
resolvedMode = mode;
}
document.documentElement.setAttribute(RXThemeMode.config.htmlAttribute, resolvedMode);
document.documentElement.setAttribute(RXThemeMode.config.htmlResolvedAttribute, resolvedMode);
document.documentElement.setAttribute(RXThemeMode.config.htmlModeAttribute, mode);
document.documentElement.style.colorScheme = resolvedMode;
})();
/**
* DOM ready helper
*/
function ready(callback) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callback);
} else {
callback();
}
}
ready(function () {
RXThemeMode.init();
});
/**
* Expose public API
*/
window.RXThemeMode = RXThemeMode;
})();
Add this small CSS in your main CSS file, for example:
assets/static-css/base/theme-mode.css
html.rx-theme-changing,
html.rx-theme-changing *,
html.rx-theme-changing *::before,
html.rx-theme-changing *::after {
transition:
background-color 250ms ease,
color 250ms ease,
border-color 250ms ease,
box-shadow 250ms ease,
fill 250ms ease,
stroke 250ms ease !important;
}
html[data-rx-theme="light"] {
color-scheme: light;
}
html[data-rx-theme="dark"] {
color-scheme: dark;
}
html[data-rx-theme="dark"] body {
background: var(--rx-color-body-bg-dark, #0f172a);
color: var(--rx-color-body-text-dark, #e5e7eb);
}
html[data-rx-theme="light"] body {
background: var(--rx-color-body-bg-light, #ffffff);
color: var(--rx-color-body-text-light, #111827);
}
.rx-theme-mode-ready [data-rx-theme-toggle],
.rx-theme-mode-ready [data-rx-theme-mode-control],
.rx-theme-mode-ready [data-theme-toggle],
.rx-theme-mode-ready [data-theme-mode] {
cursor: pointer;
}
Example buttons for your header:
<button type="button" data-rx-theme-mode-control="light">
Light
</button>
<button type="button" data-rx-theme-mode-control="dark">
Dark
</button>
<button type="button" data-rx-theme-mode-control="system">
System
</button>
<button type="button" data-rx-theme-toggle="cycle">
Theme
</button>
Example select dropdown:
<select data-rx-theme-select aria-label="Select theme mode">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
Example JS usage anywhere:
window.RXThemeMode.setDark();
window.RXThemeMode.setLight();
window.RXThemeMode.setSystem();
window.RXThemeMode.toggle();
window.RXThemeMode.cycle();
console.log(window.RXThemeMode.getMode());
For RX Theme, this file name is good:
chunk-033-theme-mode.js
And this is the best loading style in WordPress footer:
wp_enqueue_script(
'rx-theme-mode',
get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-033-theme-mode.js',
array(),
wp_get_theme()->get('Version'),
true
);
For best no-flash dark mode, later you can add a very tiny inline head script before CSS loads, but this chunk is already strong for the advanced stage.