chunk-033-theme-mode.js

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.

Leave a Reply

Your email address will not be published. Required fields are marked *