chunk-002-header.js

I’ll make this as a strong vanilla JavaScript header chunk for RX Theme: accessible menu, sticky header, search overlay, dropdowns, scroll progress, active links, back-to-top, dark mode hooks, and safe WordPress-friendly behavior.

Dear friend, create this file:

assets/static-js/static-chunks-js/chunk-002-header.js

Then paste the full code below.

/*!
 * RX Theme - Header Chunk JS
 * File: assets/static/static-chunks-js/chunk-002-header.js
 * Purpose: Advanced header, navigation, search, accessibility, sticky behavior,
 * mobile menu, dropdown menu, scroll progress, active links, back-to-top,
 * dark mode toggle, reading progress, and performance-safe UI handling.
 *
 * Author: RX Theme
 */

(function () {
  'use strict';

  /**
   * ------------------------------------------------------------
   * RX Header Config
   * ------------------------------------------------------------
   * You can change selector names here without touching all code.
   */
  const RX_HEADER_CONFIG = {
    header: '.rx-site-header',
    headerInner: '.rx-site-header__inner',

    nav: '.rx-primary-nav',
    navMenu: '.rx-primary-nav__menu',
    navLinks: '.rx-primary-nav a',

    mobileToggle: '.rx-header-menu-toggle',
    mobileClose: '.rx-header-menu-close',
    mobileOverlay: '.rx-mobile-menu-overlay',

    dropdownParent: '.menu-item-has-children, .rx-has-dropdown',
    dropdownToggle: '.rx-dropdown-toggle',
    dropdownMenu: '.sub-menu, .rx-dropdown-menu',

    searchToggle: '.rx-search-toggle',
    searchClose: '.rx-search-close',
    searchOverlay: '.rx-search-overlay',
    searchInput: '.rx-search-overlay input[type="search"], .rx-search-overlay .search-field',

    darkModeToggle: '.rx-dark-mode-toggle',

    progressBar: '.rx-scroll-progress-bar',
    backToTop: '.rx-back-to-top',

    skipLink: '.skip-link, .rx-skip-link',
    mainContent: '#main, main, .rx-main',

    activeClass: 'is-active',
    openClass: 'is-open',
    stickyClass: 'is-sticky',
    scrolledClass: 'is-scrolled',
    hiddenClass: 'is-hidden',
    lockedClass: 'rx-scroll-locked',
    dropdownOpenClass: 'is-dropdown-open',

    stickyOffset: 80,
    scrollHideOffset: 220,
    resizeDebounce: 160,
    scrollThrottle: 16
  };

  /**
   * ------------------------------------------------------------
   * Small Utility Helpers
   * ------------------------------------------------------------
   */
  const RX = {
    qs(selector, scope = document) {
      return scope.querySelector(selector);
    },

    qsa(selector, scope = document) {
      return Array.prototype.slice.call(scope.querySelectorAll(selector));
    },

    has(element, className) {
      return element && element.classList.contains(className);
    },

    add(element, className) {
      if (element) element.classList.add(className);
    },

    remove(element, className) {
      if (element) element.classList.remove(className);
    },

    toggle(element, className, force) {
      if (element) element.classList.toggle(className, force);
    },

    attr(element, name, value) {
      if (!element) return null;

      if (typeof value === 'undefined') {
        return element.getAttribute(name);
      }

      element.setAttribute(name, value);
      return value;
    },

    removeAttr(element, name) {
      if (element) element.removeAttribute(name);
    },

    on(element, event, callback, options) {
      if (!element) return;
      element.addEventListener(event, callback, options || false);
    },

    off(element, event, callback, options) {
      if (!element) return;
      element.removeEventListener(event, callback, options || false);
    },

    debounce(callback, delay) {
      let timer = null;

      return function debounced() {
        const context = this;
        const args = arguments;

        clearTimeout(timer);
        timer = setTimeout(function () {
          callback.apply(context, args);
        }, delay);
      };
    },

    throttle(callback, limit) {
      let waiting = false;

      return function throttled() {
        if (!waiting) {
          callback.apply(this, arguments);
          waiting = true;

          requestAnimationFrame(function () {
            waiting = false;
          });
        }
      };
    },

    isVisible(element) {
      if (!element) return false;
      return Boolean(
        element.offsetWidth ||
        element.offsetHeight ||
        element.getClientRects().length
      );
    },

    getFocusable(container) {
      if (!container) return [];

      return RX.qsa(
        [
          'a[href]',
          'area[href]',
          'button:not([disabled])',
          'input:not([disabled]):not([type="hidden"])',
          'select:not([disabled])',
          'textarea:not([disabled])',
          'iframe',
          'object',
          'embed',
          '[contenteditable]',
          '[tabindex]:not([tabindex="-1"])'
        ].join(','),
        container
      ).filter(RX.isVisible);
    },

    prefersReducedMotion() {
      return window.matchMedia &&
        window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    },

    isDesktop() {
      return window.matchMedia &&
        window.matchMedia('(min-width: 992px)').matches;
    }
  };

  /**
   * ------------------------------------------------------------
   * DOM Cache
   * ------------------------------------------------------------
   */
  const DOM = {
    html: document.documentElement,
    body: document.body,
    header: null,
    headerInner: null,
    nav: null,
    navMenu: null,
    mobileToggle: null,
    mobileClose: null,
    mobileOverlay: null,
    searchToggle: null,
    searchClose: null,
    searchOverlay: null,
    searchInput: null,
    darkModeToggle: null,
    progressBar: null,
    backToTop: null,
    mainContent: null
  };

  /**
   * ------------------------------------------------------------
   * State
   * ------------------------------------------------------------
   */
  const STATE = {
    lastScrollY: window.scrollY || 0,
    ticking: false,
    mobileMenuOpen: false,
    searchOpen: false,
    activeTrap: null,
    previousFocus: null
  };

  /**
   * ------------------------------------------------------------
   * Cache Elements
   * ------------------------------------------------------------
   */
  function cacheElements() {
    DOM.header = RX.qs(RX_HEADER_CONFIG.header);
    DOM.headerInner = RX.qs(RX_HEADER_CONFIG.headerInner);
    DOM.nav = RX.qs(RX_HEADER_CONFIG.nav);
    DOM.navMenu = RX.qs(RX_HEADER_CONFIG.navMenu);
    DOM.mobileToggle = RX.qs(RX_HEADER_CONFIG.mobileToggle);
    DOM.mobileClose = RX.qs(RX_HEADER_CONFIG.mobileClose);
    DOM.mobileOverlay = RX.qs(RX_HEADER_CONFIG.mobileOverlay);
    DOM.searchToggle = RX.qs(RX_HEADER_CONFIG.searchToggle);
    DOM.searchClose = RX.qs(RX_HEADER_CONFIG.searchClose);
    DOM.searchOverlay = RX.qs(RX_HEADER_CONFIG.searchOverlay);
    DOM.searchInput = RX.qs(RX_HEADER_CONFIG.searchInput);
    DOM.darkModeToggle = RX.qs(RX_HEADER_CONFIG.darkModeToggle);
    DOM.progressBar = RX.qs(RX_HEADER_CONFIG.progressBar);
    DOM.backToTop = RX.qs(RX_HEADER_CONFIG.backToTop);
    DOM.mainContent = RX.qs(RX_HEADER_CONFIG.mainContent);
  }

  /**
   * ------------------------------------------------------------
   * Body Scroll Lock
   * ------------------------------------------------------------
   */
  function lockBodyScroll() {
    RX.add(DOM.html, RX_HEADER_CONFIG.lockedClass);
    RX.add(DOM.body, RX_HEADER_CONFIG.lockedClass);
  }

  function unlockBodyScroll() {
    RX.remove(DOM.html, RX_HEADER_CONFIG.lockedClass);
    RX.remove(DOM.body, RX_HEADER_CONFIG.lockedClass);
  }

  /**
   * ------------------------------------------------------------
   * Focus Trap for Mobile Menu and Search Overlay
   * ------------------------------------------------------------
   */
  function activateFocusTrap(container) {
    if (!container) return;

    STATE.activeTrap = container;
    STATE.previousFocus = document.activeElement;

    RX.on(document, 'keydown', handleFocusTrap);
  }

  function deactivateFocusTrap() {
    RX.off(document, 'keydown', handleFocusTrap);

    if (STATE.previousFocus && typeof STATE.previousFocus.focus === 'function') {
      STATE.previousFocus.focus({ preventScroll: true });
    }

    STATE.activeTrap = null;
    STATE.previousFocus = null;
  }

  function handleFocusTrap(event) {
    if (!STATE.activeTrap || event.key !== 'Tab') return;

    const focusable = RX.getFocusable(STATE.activeTrap);

    if (!focusable.length) {
      event.preventDefault();
      return;
    }

    const first = focusable[0];
    const last = focusable[focusable.length - 1];

    if (event.shiftKey && document.activeElement === first) {
      event.preventDefault();
      last.focus();
    } else if (!event.shiftKey && document.activeElement === last) {
      event.preventDefault();
      first.focus();
    }
  }

  /**
   * ------------------------------------------------------------
   * Mobile Menu
   * ------------------------------------------------------------
   */
  function openMobileMenu() {
    if (!DOM.nav && !DOM.mobileOverlay) return;

    STATE.mobileMenuOpen = true;

    RX.add(DOM.body, 'rx-mobile-menu-active');
    RX.add(DOM.nav, RX_HEADER_CONFIG.openClass);
    RX.add(DOM.mobileOverlay, RX_HEADER_CONFIG.openClass);

    RX.attr(DOM.mobileToggle, 'aria-expanded', 'true');
    RX.attr(DOM.nav, 'aria-hidden', 'false');

    lockBodyScroll();

    const focusTarget =
      RX.qs('a, button, input, [tabindex]:not([tabindex="-1"])', DOM.nav) ||
      DOM.mobileClose ||
      DOM.nav;

    if (focusTarget && typeof focusTarget.focus === 'function') {
      focusTarget.focus({ preventScroll: true });
    }

    activateFocusTrap(DOM.nav);
  }

  function closeMobileMenu() {
    STATE.mobileMenuOpen = false;

    RX.remove(DOM.body, 'rx-mobile-menu-active');
    RX.remove(DOM.nav, RX_HEADER_CONFIG.openClass);
    RX.remove(DOM.mobileOverlay, RX_HEADER_CONFIG.openClass);

    RX.attr(DOM.mobileToggle, 'aria-expanded', 'false');
    RX.attr(DOM.nav, 'aria-hidden', 'true');

    closeAllDropdowns();

    unlockBodyScroll();
    deactivateFocusTrap();
  }

  function toggleMobileMenu() {
    if (STATE.mobileMenuOpen) {
      closeMobileMenu();
    } else {
      openMobileMenu();
    }
  }

  function setupMobileMenu() {
    if (DOM.mobileToggle) {
      RX.attr(DOM.mobileToggle, 'aria-expanded', 'false');
      RX.attr(DOM.mobileToggle, 'aria-controls', 'rx-primary-navigation');
      RX.on(DOM.mobileToggle, 'click', toggleMobileMenu);
    }

    if (DOM.mobileClose) {
      RX.on(DOM.mobileClose, 'click', closeMobileMenu);
    }

    if (DOM.mobileOverlay) {
      RX.on(DOM.mobileOverlay, 'click', closeMobileMenu);
    }

    if (DOM.nav) {
      RX.attr(DOM.nav, 'id', RX.attr(DOM.nav, 'id') || 'rx-primary-navigation');

      if (!RX.isDesktop()) {
        RX.attr(DOM.nav, 'aria-hidden', 'true');
      }
    }

    RX.qsa(RX_HEADER_CONFIG.navLinks).forEach(function (link) {
      RX.on(link, 'click', function () {
        if (!RX.isDesktop()) {
          closeMobileMenu();
        }
      });
    });
  }

  /**
   * ------------------------------------------------------------
   * Dropdown Menu
   * ------------------------------------------------------------
   */
  function setupDropdowns() {
    const dropdownParents = RX.qsa(RX_HEADER_CONFIG.dropdownParent, DOM.nav || document);

    dropdownParents.forEach(function (parent, index) {
      const submenu = RX.qs(RX_HEADER_CONFIG.dropdownMenu, parent);
      let toggle = RX.qs(RX_HEADER_CONFIG.dropdownToggle, parent);
      const firstLink = RX.qs('a', parent);

      if (!submenu) return;

      const submenuId = submenu.id || 'rx-submenu-' + index;
      submenu.id = submenuId;

      if (!toggle && firstLink) {
        toggle = document.createElement('button');
        toggle.className = 'rx-dropdown-toggle';
        toggle.type = 'button';
        toggle.setAttribute('aria-label', 'Open submenu');
        toggle.innerHTML = '<span aria-hidden="true">+</span>';
        firstLink.insertAdjacentElement('afterend', toggle);
      }

      if (toggle) {
        RX.attr(toggle, 'aria-expanded', 'false');
        RX.attr(toggle, 'aria-controls', submenuId);

        RX.on(toggle, 'click', function (event) {
          event.preventDefault();
          event.stopPropagation();

          const isOpen = RX.has(parent, RX_HEADER_CONFIG.dropdownOpenClass);

          if (RX.isDesktop()) {
            closeSiblingDropdowns(parent);
          }

          if (isOpen) {
            closeDropdown(parent);
          } else {
            openDropdown(parent);
          }
        });
      }

      RX.on(parent, 'mouseenter', function () {
        if (RX.isDesktop()) openDropdown(parent);
      });

      RX.on(parent, 'mouseleave', function () {
        if (RX.isDesktop()) closeDropdown(parent);
      });

      RX.on(parent, 'keydown', function (event) {
        if (event.key === 'Escape') {
          closeDropdown(parent);

          if (toggle) {
            toggle.focus();
          }
        }
      });
    });

    RX.on(document, 'click', function (event) {
      if (!DOM.nav) return;

      if (!DOM.nav.contains(event.target)) {
        closeAllDropdowns();
      }
    });
  }

  function openDropdown(parent) {
    const toggle = RX.qs(RX_HEADER_CONFIG.dropdownToggle, parent);

    RX.add(parent, RX_HEADER_CONFIG.dropdownOpenClass);

    if (toggle) {
      RX.attr(toggle, 'aria-expanded', 'true');
    }
  }

  function closeDropdown(parent) {
    const toggle = RX.qs(RX_HEADER_CONFIG.dropdownToggle, parent);

    RX.remove(parent, RX_HEADER_CONFIG.dropdownOpenClass);

    if (toggle) {
      RX.attr(toggle, 'aria-expanded', 'false');
    }
  }

  function closeSiblingDropdowns(parent) {
    const siblings = Array.prototype.filter.call(parent.parentNode.children, function (child) {
      return child !== parent;
    });

    siblings.forEach(function (sibling) {
      closeDropdown(sibling);
    });
  }

  function closeAllDropdowns() {
    RX.qsa(RX_HEADER_CONFIG.dropdownParent, DOM.nav || document).forEach(closeDropdown);
  }

  /**
   * ------------------------------------------------------------
   * Search Overlay
   * ------------------------------------------------------------
   */
  function openSearch() {
    if (!DOM.searchOverlay) return;

    STATE.searchOpen = true;

    RX.add(DOM.body, 'rx-search-active');
    RX.add(DOM.searchOverlay, RX_HEADER_CONFIG.openClass);

    RX.attr(DOM.searchToggle, 'aria-expanded', 'true');
    RX.attr(DOM.searchOverlay, 'aria-hidden', 'false');

    lockBodyScroll();

    window.setTimeout(function () {
      if (DOM.searchInput) {
        DOM.searchInput.focus();
      }
    }, 80);

    activateFocusTrap(DOM.searchOverlay);
  }

  function closeSearch() {
    if (!DOM.searchOverlay) return;

    STATE.searchOpen = false;

    RX.remove(DOM.body, 'rx-search-active');
    RX.remove(DOM.searchOverlay, RX_HEADER_CONFIG.openClass);

    RX.attr(DOM.searchToggle, 'aria-expanded', 'false');
    RX.attr(DOM.searchOverlay, 'aria-hidden', 'true');

    unlockBodyScroll();
    deactivateFocusTrap();
  }

  function toggleSearch() {
    if (STATE.searchOpen) {
      closeSearch();
    } else {
      openSearch();
    }
  }

  function setupSearchOverlay() {
    if (DOM.searchToggle) {
      RX.attr(DOM.searchToggle, 'aria-expanded', 'false');
      RX.on(DOM.searchToggle, 'click', function (event) {
        event.preventDefault();
        toggleSearch();
      });
    }

    if (DOM.searchClose) {
      RX.on(DOM.searchClose, 'click', function (event) {
        event.preventDefault();
        closeSearch();
      });
    }

    if (DOM.searchOverlay) {
      RX.attr(DOM.searchOverlay, 'aria-hidden', 'true');

      RX.on(DOM.searchOverlay, 'click', function (event) {
        if (event.target === DOM.searchOverlay) {
          closeSearch();
        }
      });
    }
  }

  /**
   * ------------------------------------------------------------
   * Sticky Header and Hide-on-scroll
   * ------------------------------------------------------------
   */
  function updateStickyHeader() {
    if (!DOM.header) return;

    const currentY = window.scrollY || window.pageYOffset;
    const isScrolled = currentY > RX_HEADER_CONFIG.stickyOffset;
    const scrollingDown = currentY > STATE.lastScrollY;
    const shouldHide =
      scrollingDown &&
      currentY > RX_HEADER_CONFIG.scrollHideOffset &&
      !STATE.mobileMenuOpen &&
      !STATE.searchOpen;

    RX.toggle(DOM.header, RX_HEADER_CONFIG.scrolledClass, isScrolled);
    RX.toggle(DOM.header, RX_HEADER_CONFIG.stickyClass, isScrolled);
    RX.toggle(DOM.header, RX_HEADER_CONFIG.hiddenClass, shouldHide);

    STATE.lastScrollY = currentY;
  }

  /**
   * ------------------------------------------------------------
   * Scroll Progress Bar
   * ------------------------------------------------------------
   */
  function updateScrollProgress() {
    if (!DOM.progressBar) return;

    const scrollTop = window.scrollY || document.documentElement.scrollTop;
    const docHeight =
      document.documentElement.scrollHeight -
      document.documentElement.clientHeight;

    const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;

    DOM.progressBar.style.width = Math.min(100, Math.max(0, progress)) + '%';
  }

  /**
   * ------------------------------------------------------------
   * Back to Top Button
   * ------------------------------------------------------------
   */
  function updateBackToTop() {
    if (!DOM.backToTop) return;

    const show = (window.scrollY || window.pageYOffset) > 500;

    RX.toggle(DOM.backToTop, RX_HEADER_CONFIG.activeClass, show);
    RX.attr(DOM.backToTop, 'aria-hidden', show ? 'false' : 'true');
  }

  function setupBackToTop() {
    if (!DOM.backToTop) return;

    RX.attr(DOM.backToTop, 'aria-hidden', 'true');

    RX.on(DOM.backToTop, 'click', function (event) {
      event.preventDefault();

      window.scrollTo({
        top: 0,
        behavior: RX.prefersReducedMotion() ? 'auto' : 'smooth'
      });
    });
  }

  /**
   * ------------------------------------------------------------
   * Active Current Menu Link
   * ------------------------------------------------------------
   */
  function setupActiveMenuLinks() {
    const links = RX.qsa(RX_HEADER_CONFIG.navLinks);
    const currentUrl = normalizeUrl(window.location.href);

    links.forEach(function (link) {
      const href = link.href;

      if (!href) return;

      const linkUrl = normalizeUrl(href);

      if (linkUrl === currentUrl) {
        RX.add(link, RX_HEADER_CONFIG.activeClass);
        RX.attr(link, 'aria-current', 'page');

        const parentLi = link.closest('li');

        if (parentLi) {
          RX.add(parentLi, 'current-menu-item');
        }
      }
    });
  }

  function normalizeUrl(url) {
    try {
      const parsed = new URL(url);
      return parsed.origin + parsed.pathname.replace(/\/+$/, '');
    } catch (error) {
      return url;
    }
  }

  /**
   * ------------------------------------------------------------
   * Smooth Anchor Scrolling
   * ------------------------------------------------------------
   */
  function setupSmoothAnchors() {
    RX.qsa('a[href^="#"]').forEach(function (link) {
      RX.on(link, 'click', function (event) {
        const targetId = link.getAttribute('href');

        if (!targetId || targetId === '#') return;

        const target = RX.qs(targetId);

        if (!target) return;

        event.preventDefault();

        const headerHeight = DOM.header ? DOM.header.offsetHeight : 0;
        const targetPosition =
          target.getBoundingClientRect().top +
          window.pageYOffset -
          headerHeight -
          16;

        window.scrollTo({
          top: targetPosition,
          behavior: RX.prefersReducedMotion() ? 'auto' : 'smooth'
        });

        target.setAttribute('tabindex', '-1');
        target.focus({ preventScroll: true });
      });
    });
  }

  /**
   * ------------------------------------------------------------
   * Skip Link Improvement
   * ------------------------------------------------------------
   */
  function setupSkipLinks() {
    const skipLinks = RX.qsa(RX_HEADER_CONFIG.skipLink);

    skipLinks.forEach(function (link) {
      RX.on(link, 'click', function () {
        const target = DOM.mainContent;

        if (!target) return;

        target.setAttribute('tabindex', '-1');

        window.setTimeout(function () {
          target.focus();
        }, 10);
      });
    });
  }

  /**
   * ------------------------------------------------------------
   * Dark Mode Toggle
   * ------------------------------------------------------------
   * This uses localStorage safely.
   * CSS should use:
   * html[data-rx-theme="dark"] { ... }
   */
  function setupDarkMode() {
    if (!DOM.darkModeToggle) return;

    const savedTheme = safeLocalStorageGet('rx-theme-mode');
    const systemDark =
      window.matchMedia &&
      window.matchMedia('(prefers-color-scheme: dark)').matches;

    const initialTheme = savedTheme || (systemDark ? 'dark' : 'light');

    applyTheme(initialTheme);

    RX.on(DOM.darkModeToggle, 'click', function (event) {
      event.preventDefault();

      const current = DOM.html.getAttribute('data-rx-theme') || 'light';
      const next = current === 'dark' ? 'light' : 'dark';

      applyTheme(next);
      safeLocalStorageSet('rx-theme-mode', next);
    });
  }

  function applyTheme(theme) {
    DOM.html.setAttribute('data-rx-theme', theme);

    if (DOM.darkModeToggle) {
      RX.attr(DOM.darkModeToggle, 'aria-pressed', theme === 'dark' ? 'true' : 'false');
      RX.attr(
        DOM.darkModeToggle,
        'aria-label',
        theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'
      );
    }
  }

  function safeLocalStorageGet(key) {
    try {
      return window.localStorage.getItem(key);
    } catch (error) {
      return null;
    }
  }

  function safeLocalStorageSet(key, value) {
    try {
      window.localStorage.setItem(key, value);
    } catch (error) {
      return false;
    }

    return true;
  }

  /**
   * ------------------------------------------------------------
   * Header Height CSS Variable
   * ------------------------------------------------------------
   * CSS can use:
   * padding-top: var(--rx-header-height);
   */
  function updateHeaderHeightVariable() {
    if (!DOM.header) return;

    const height = DOM.header.offsetHeight || 0;
    DOM.html.style.setProperty('--rx-header-height', height + 'px');
  }

  /**
   * ------------------------------------------------------------
   * Keyboard Global Events
   * ------------------------------------------------------------
   */
  function setupKeyboardEvents() {
    RX.on(document, 'keydown', function (event) {
      if (event.key === 'Escape') {
        if (STATE.searchOpen) {
          closeSearch();
        }

        if (STATE.mobileMenuOpen) {
          closeMobileMenu();
        }

        closeAllDropdowns();
      }
    });
  }

  /**
   * ------------------------------------------------------------
   * Header Scroll Handler
   * ------------------------------------------------------------
   */
  function handleScroll() {
    updateStickyHeader();
    updateScrollProgress();
    updateBackToTop();
  }

  /**
   * ------------------------------------------------------------
   * Resize Handler
   * ------------------------------------------------------------
   */
  function handleResize() {
    updateHeaderHeightVariable();

    if (RX.isDesktop() && STATE.mobileMenuOpen) {
      closeMobileMenu();
    }
  }

  /**
   * ------------------------------------------------------------
   * WordPress Admin Bar Support
   * ------------------------------------------------------------
   */
  function setupAdminBarOffset() {
    const adminBar = RX.qs('#wpadminbar');

    if (!adminBar) return;

    const adminBarHeight = adminBar.offsetHeight || 0;
    DOM.html.style.setProperty('--rx-admin-bar-height', adminBarHeight + 'px');
    RX.add(DOM.body, 'rx-has-admin-bar');
  }

  /**
   * ------------------------------------------------------------
   * Lazy Header Shadow Enhancement
   * ------------------------------------------------------------
   */
  function setupHeaderShadowObserver() {
    if (!DOM.header || !window.IntersectionObserver) return;

    const sentinel = document.createElement('div');
    sentinel.className = 'rx-header-sentinel';
    sentinel.setAttribute('aria-hidden', 'true');

    DOM.header.parentNode.insertBefore(sentinel, DOM.header);

    const observer = new IntersectionObserver(
      function (entries) {
        entries.forEach(function (entry) {
          RX.toggle(DOM.header, 'has-shadow', !entry.isIntersecting);
        });
      },
      {
        root: null,
        threshold: 0
      }
    );

    observer.observe(sentinel);
  }

  /**
   * ------------------------------------------------------------
   * Header Search Form Empty Submit Protection
   * ------------------------------------------------------------
   */
  function setupSearchFormValidation() {
    if (!DOM.searchOverlay) return;

    const forms = RX.qsa('form[role="search"], form.search-form', DOM.searchOverlay);

    forms.forEach(function (form) {
      RX.on(form, 'submit', function (event) {
        const input = RX.qs('input[type="search"], .search-field', form);

        if (!input) return;

        if (!input.value.trim()) {
          event.preventDefault();
          input.focus();
          RX.add(form, 'has-error');

          window.setTimeout(function () {
            RX.remove(form, 'has-error');
          }, 1600);
        }
      });
    });
  }

  /**
   * ------------------------------------------------------------
   * Mobile Submenu Auto Close on Link Click
   * ------------------------------------------------------------
   */
  function setupMobileSubmenuLinkBehavior() {
    RX.qsa(RX_HEADER_CONFIG.dropdownMenu + ' a', DOM.nav || document).forEach(function (link) {
      RX.on(link, 'click', function () {
        if (!RX.isDesktop()) {
          closeMobileMenu();
        }
      });
    });
  }

  /**
   * ------------------------------------------------------------
   * Add Loaded Class
   * ------------------------------------------------------------
   */
  function markHeaderLoaded() {
    RX.add(DOM.body, 'rx-header-js-loaded');

    if (DOM.header) {
      RX.add(DOM.header, 'is-ready');
    }
  }

  /**
   * ------------------------------------------------------------
   * Main Init
   * ------------------------------------------------------------
   */
  function initRXHeader() {
    cacheElements();

    if (!DOM.header) {
      return;
    }

    setupAdminBarOffset();
    setupMobileMenu();
    setupDropdowns();
    setupSearchOverlay();
    setupSearchFormValidation();
    setupBackToTop();
    setupDarkMode();
    setupActiveMenuLinks();
    setupSmoothAnchors();
    setupSkipLinks();
    setupKeyboardEvents();
    setupHeaderShadowObserver();
    setupMobileSubmenuLinkBehavior();

    updateHeaderHeightVariable();
    handleScroll();
    markHeaderLoaded();

    RX.on(
      window,
      'scroll',
      RX.throttle(handleScroll, RX_HEADER_CONFIG.scrollThrottle),
      { passive: true }
    );

    RX.on(
      window,
      'resize',
      RX.debounce(handleResize, RX_HEADER_CONFIG.resizeDebounce)
    );

    RX.on(window, 'orientationchange', function () {
      window.setTimeout(handleResize, 250);
    });
  }

  /**
   * ------------------------------------------------------------
   * DOM Ready
   * ------------------------------------------------------------
   */
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initRXHeader);
  } else {
    initRXHeader();
  }
})();

Use these matching class names in your header HTML/PHP when possible:

<header class="rx-site-header">
  <div class="rx-site-header__inner">

    <button class="rx-header-menu-toggle" type="button" aria-label="Open menu">
      Menu
    </button>

    <nav class="rx-primary-nav" aria-label="Primary menu">
      <?php
      wp_nav_menu([
        'theme_location' => 'primary',
        'menu_class'     => 'rx-primary-nav__menu',
        'container'      => false,
      ]);
      ?>
    </nav>

    <button class="rx-search-toggle" type="button" aria-label="Open search">
      Search
    </button>

    <button class="rx-dark-mode-toggle" type="button">
      Mode
    </button>

  </div>
</header>

<div class="rx-mobile-menu-overlay" aria-hidden="true"></div>

<div class="rx-search-overlay" aria-hidden="true">
  <button class="rx-search-close" type="button" aria-label="Close search">Close</button>
  <?php get_search_form(); ?>
</div>

<div class="rx-scroll-progress-bar" aria-hidden="true"></div>

<button class="rx-back-to-top" type="button" aria-label="Back to top"></button>

And enqueue it like this in your theme:

function rx_theme_enqueue_header_chunk() {
    wp_enqueue_script(
        'rx-header-chunk',
        get_template_directory_uri() . '/assets/static/static-chunks-js/chunk-002-header.js',
        array(),
        wp_get_theme()->get('Version'),
        true
    );
}
add_action('wp_enqueue_scripts', 'rx_theme_enqueue_header_chunk');

Important CSS helper classes you should later support:

.rx-scroll-locked {
  overflow: hidden;
}

.rx-site-header {
  position: sticky;
  top: var(--rx-admin-bar-height, 0px);
  z-index: 999;
}

.rx-site-header.is-hidden {
  transform: translateY(-100%);
}

.rx-primary-nav.is-open {
  visibility: visible;
  opacity: 1;
}

.rx-search-overlay {
  visibility: hidden;
  opacity: 0;
}

.rx-search-overlay.is-open {
  visibility: visible;
  opacity: 1;
}

.rx-scroll-progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  width: 0;
  z-index: 9999;
}

.rx-back-to-top {
  opacity: 0;
  pointer-events: none;
}

.rx-back-to-top.is-active {
  opacity: 1;
  pointer-events: auto;
}

html[data-rx-theme="dark"] {
  color-scheme: dark;
}

This is a strong advanced starting file for chunk-002-header.js. It is big enough for a professional theme, but still safe, fast, and not dependent on jQuery.

Leave a Reply

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