chunk-023-sticky-sidebar.js

I’ll build this as a standalone advanced sticky sidebar module for your RX Theme, using safe vanilla JavaScript so it works without jQuery and stays performance-friendly.

Friend, create this file:

assets/static-js/static-chunks-js/chunk-023-sticky-sidebar.js

Then paste this full advanced code:

/*!
 * RX Theme - Sticky Sidebar Advanced Chunk
 * File: assets/static-js/static-chunks-js/chunk-023-sticky-sidebar.js
 * Version: 1.0.0
 *
 * Features:
 * - Vanilla JS, no jQuery
 * - Auto sticky sidebar
 * - Header/admin-bar offset support
 * - Footer boundary detection
 * - Smooth position calculation
 * - Responsive disable on mobile
 * - Resize/orientation support
 * - MutationObserver support
 * - RTL support
 * - Reduced motion friendly
 * - WordPress admin bar compatible
 * - Safe fallback for old browsers
 */

(function () {
  'use strict';

  /**
   * Prevent duplicate initialization.
   */
  if (window.RXStickySidebarLoaded) {
    return;
  }

  window.RXStickySidebarLoaded = true;

  /**
   * Main RX Sticky Sidebar object.
   */
  var RXStickySidebar = {
    instances: [],
    ticking: false,
    resizeTimer: null,
    mutationTimer: null,

    config: {
      sidebarSelector: '.rx-sidebar, .sidebar, aside.widget-area, #secondary',
      contentSelector: '.rx-content, .site-main, main, #primary',
      containerSelector: '.rx-layout, .site-content, .content-area, .rx-main-wrapper',
      headerSelector: '.rx-header, .site-header, header, #masthead',
      footerSelector: '.rx-footer, .site-footer, footer, #colophon',
      stickyClass: 'rx-is-sticky-sidebar',
      stuckClass: 'rx-sidebar-stuck',
      bottomClass: 'rx-sidebar-bottom',
      disabledClass: 'rx-sticky-disabled',
      placeholderClass: 'rx-sticky-placeholder',
      minWidth: 992,
      topGap: 24,
      bottomGap: 24,
      headerGap: 16,
      adminBarGap: 8,
      zIndex: 20,
      enableOnTouch: true,
      respectReducedMotion: true,
      debug: false
    },

    /**
     * Start module.
     */
    init: function () {
      if (!this.isBrowserReady()) {
        return;
      }

      this.injectCSS();
      this.collectSidebars();
      this.bindEvents();
      this.observeDOM();
      this.refresh();

      this.log('RX Sticky Sidebar initialized.');
    },

    /**
     * Basic browser safety.
     */
    isBrowserReady: function () {
      return !!(
        window &&
        document &&
        document.documentElement &&
        document.body &&
        document.querySelectorAll
      );
    },

    /**
     * Debug logger.
     */
    log: function () {
      if (!this.config.debug || !window.console) {
        return;
      }

      console.log.apply(console, arguments);
    },

    /**
     * Get merged config from global object.
     */
    getConfig: function () {
      var custom = window.RX_STICKY_SIDEBAR_CONFIG || {};
      var finalConfig = {};

      Object.keys(this.config).forEach(function (key) {
        finalConfig[key] = Object.prototype.hasOwnProperty.call(custom, key)
          ? custom[key]
          : RXStickySidebar.config[key];
      });

      return finalConfig;
    },

    /**
     * Collect all possible sidebar elements.
     */
    collectSidebars: function () {
      var cfg = this.getConfig();
      var sidebars = document.querySelectorAll(cfg.sidebarSelector);
      var self = this;

      this.instances = [];

      if (!sidebars.length) {
        this.log('No sidebar found.');
        return;
      }

      Array.prototype.forEach.call(sidebars, function (sidebar, index) {
        if (!sidebar || sidebar.dataset.rxStickyReady === '1') {
          return;
        }

        if (self.shouldIgnoreSidebar(sidebar)) {
          return;
        }

        var instance = self.createInstance(sidebar, index, cfg);

        if (instance) {
          sidebar.dataset.rxStickyReady = '1';
          self.instances.push(instance);
        }
      });
    },

    /**
     * Ignore invalid sidebar.
     */
    shouldIgnoreSidebar: function (sidebar) {
      if (!sidebar) {
        return true;
      }

      if (sidebar.closest('.rx-no-sticky-sidebar')) {
        return true;
      }

      if (sidebar.dataset.rxSticky === 'false') {
        return true;
      }

      if (sidebar.offsetHeight < 120) {
        return true;
      }

      return false;
    },

    /**
     * Create one sticky sidebar instance.
     */
    createInstance: function (sidebar, index, cfg) {
      var container = this.findContainer(sidebar, cfg);
      var content = this.findContent(sidebar, cfg);
      var placeholder = document.createElement('div');

      placeholder.className = cfg.placeholderClass;
      placeholder.style.display = 'none';
      placeholder.style.width = '100%';
      placeholder.style.height = '0px';

      sidebar.parentNode.insertBefore(placeholder, sidebar);

      return {
        id: index,
        sidebar: sidebar,
        container: container,
        content: content,
        placeholder: placeholder,
        originalStyle: {
          position: sidebar.style.position || '',
          top: sidebar.style.top || '',
          bottom: sidebar.style.bottom || '',
          left: sidebar.style.left || '',
          right: sidebar.style.right || '',
          width: sidebar.style.width || '',
          zIndex: sidebar.style.zIndex || '',
          transform: sidebar.style.transform || ''
        },
        state: 'normal',
        lastTop: 0,
        lastLeft: 0,
        lastWidth: 0,
        lastHeight: 0,
        enabled: true
      };
    },

    /**
     * Find wrapper container.
     */
    findContainer: function (sidebar, cfg) {
      var container = sidebar.closest(cfg.containerSelector);

      if (container) {
        return container;
      }

      return sidebar.parentNode || document.body;
    },

    /**
     * Find main content beside sidebar.
     */
    findContent: function (sidebar, cfg) {
      var container = sidebar.closest(cfg.containerSelector);

      if (!container) {
        return document.querySelector(cfg.contentSelector);
      }

      var content = container.querySelector(cfg.contentSelector);

      if (content && content !== sidebar) {
        return content;
      }

      return null;
    },

    /**
     * Bind scroll, resize, load events.
     */
    bindEvents: function () {
      var self = this;

      window.addEventListener(
        'scroll',
        function () {
          self.requestUpdate();
        },
        { passive: true }
      );

      window.addEventListener(
        'resize',
        function () {
          clearTimeout(self.resizeTimer);
          self.resizeTimer = setTimeout(function () {
            self.resetAll();
            self.refresh();
          }, 120);
        },
        { passive: true }
      );

      window.addEventListener(
        'orientationchange',
        function () {
          setTimeout(function () {
            self.resetAll();
            self.refresh();
          }, 250);
        },
        { passive: true }
      );

      window.addEventListener('load', function () {
        self.resetAll();
        self.refresh();
      });

      document.addEventListener('DOMContentLoaded', function () {
        self.refresh();
      });
    },

    /**
     * Observe layout changes.
     */
    observeDOM: function () {
      var self = this;

      if (!('MutationObserver' in window)) {
        return;
      }

      var observer = new MutationObserver(function () {
        clearTimeout(self.mutationTimer);
        self.mutationTimer = setTimeout(function () {
          self.collectSidebars();
          self.refresh();
        }, 300);
      });

      observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: false
      });
    },

    /**
     * Request animation-frame update.
     */
    requestUpdate: function () {
      var self = this;

      if (self.ticking) {
        return;
      }

      self.ticking = true;

      window.requestAnimationFrame(function () {
        self.update();
        self.ticking = false;
      });
    },

    /**
     * Refresh all instances.
     */
    refresh: function () {
      this.update();
    },

    /**
     * Main update loop.
     */
    update: function () {
      var self = this;

      if (!this.instances.length) {
        return;
      }

      this.instances.forEach(function (instance) {
        self.updateInstance(instance);
      });
    },

    /**
     * Update one sticky sidebar.
     */
    updateInstance: function (instance) {
      var cfg = this.getConfig();

      if (!instance || !instance.sidebar) {
        return;
      }

      if (!this.canEnable(instance, cfg)) {
        this.disableInstance(instance, cfg);
        return;
      }

      instance.enabled = true;

      var sidebar = instance.sidebar;
      var container = instance.container;
      var placeholder = instance.placeholder;

      var scrollY = this.getScrollY();
      var viewportHeight = window.innerHeight || document.documentElement.clientHeight;
      var adminOffset = this.getAdminBarOffset(cfg);
      var headerOffset = this.getHeaderOffset(cfg);
      var topOffset = adminOffset + headerOffset + cfg.topGap;
      var containerRect = this.getDocumentRect(container);
      var sidebarRect = this.getDocumentRect(sidebar);
      var placeholderRect = this.getDocumentRect(placeholder);

      var naturalTop = placeholderRect.top || sidebarRect.top;
      var sidebarHeight = sidebar.offsetHeight;
      var sidebarWidth = placeholder.offsetWidth || sidebar.offsetWidth;
      var sidebarLeft = placeholder.getBoundingClientRect().left + window.pageXOffset;
      var containerBottom = containerRect.top + container.offsetHeight;
      var stickyStart = naturalTop - topOffset;
      var stickyEnd = containerBottom - sidebarHeight - cfg.bottomGap - topOffset;

      if (sidebarHeight + topOffset + cfg.bottomGap > viewportHeight) {
        this.disableInstance(instance, cfg);
        return;
      }

      if (scrollY <= stickyStart) {
        this.setNormal(instance, cfg);
      } else if (scrollY > stickyStart && scrollY < stickyEnd) {
        this.setFixed(instance, cfg, topOffset, sidebarWidth, sidebarLeft);
      } else {
        this.setBottom(instance, cfg, stickyEnd - containerRect.top + topOffset, sidebarWidth);
      }
    },

    /**
     * Check if sticky sidebar can run.
     */
    canEnable: function (instance, cfg) {
      if (!instance.sidebar || !instance.container) {
        return false;
      }

      if (window.innerWidth < cfg.minWidth) {
        return false;
      }

      if (!cfg.enableOnTouch && this.isTouchDevice()) {
        return false;
      }

      if (cfg.respectReducedMotion && this.prefersReducedMotion()) {
        return false;
      }

      if (instance.sidebar.offsetHeight < 120) {
        return false;
      }

      if (instance.container.offsetHeight <= instance.sidebar.offsetHeight + 80) {
        return false;
      }

      return true;
    },

    /**
     * Normal position.
     */
    setNormal: function (instance, cfg) {
      var sidebar = instance.sidebar;
      var placeholder = instance.placeholder;

      if (instance.state === 'normal') {
        return;
      }

      sidebar.style.position = instance.originalStyle.position;
      sidebar.style.top = instance.originalStyle.top;
      sidebar.style.bottom = instance.originalStyle.bottom;
      sidebar.style.left = instance.originalStyle.left;
      sidebar.style.right = instance.originalStyle.right;
      sidebar.style.width = instance.originalStyle.width;
      sidebar.style.zIndex = instance.originalStyle.zIndex;
      sidebar.style.transform = instance.originalStyle.transform;

      placeholder.style.display = 'none';
      placeholder.style.height = '0px';

      sidebar.classList.remove(cfg.stickyClass);
      sidebar.classList.remove(cfg.stuckClass);
      sidebar.classList.remove(cfg.bottomClass);
      sidebar.classList.remove(cfg.disabledClass);

      instance.state = 'normal';
    },

    /**
     * Fixed sticky position.
     */
    setFixed: function (instance, cfg, topOffset, width, left) {
      var sidebar = instance.sidebar;
      var placeholder = instance.placeholder;
      var isRTL = this.isRTL();

      sidebar.style.position = 'fixed';
      sidebar.style.top = topOffset + 'px';
      sidebar.style.bottom = 'auto';
      sidebar.style.width = width + 'px';
      sidebar.style.zIndex = String(cfg.zIndex);

      if (isRTL) {
        var right = window.innerWidth - left - width;
        sidebar.style.right = right + 'px';
        sidebar.style.left = 'auto';
      } else {
        sidebar.style.left = left + 'px';
        sidebar.style.right = 'auto';
      }

      sidebar.style.transform = 'translateZ(0)';

      placeholder.style.display = 'block';
      placeholder.style.height = sidebar.offsetHeight + 'px';

      sidebar.classList.add(cfg.stickyClass);
      sidebar.classList.add(cfg.stuckClass);
      sidebar.classList.remove(cfg.bottomClass);
      sidebar.classList.remove(cfg.disabledClass);

      instance.state = 'fixed';
      instance.lastTop = topOffset;
      instance.lastLeft = left;
      instance.lastWidth = width;
      instance.lastHeight = sidebar.offsetHeight;
    },

    /**
     * Bottom locked position.
     */
    setBottom: function (instance, cfg, top, width) {
      var sidebar = instance.sidebar;
      var placeholder = instance.placeholder;

      sidebar.style.position = 'absolute';
      sidebar.style.top = top + 'px';
      sidebar.style.bottom = 'auto';
      sidebar.style.left = '';
      sidebar.style.right = '';
      sidebar.style.width = width + 'px';
      sidebar.style.zIndex = String(cfg.zIndex);
      sidebar.style.transform = 'translateZ(0)';

      placeholder.style.display = 'block';
      placeholder.style.height = sidebar.offsetHeight + 'px';

      if (window.getComputedStyle(instance.container).position === 'static') {
        instance.container.style.position = 'relative';
      }

      sidebar.classList.add(cfg.stickyClass);
      sidebar.classList.remove(cfg.stuckClass);
      sidebar.classList.add(cfg.bottomClass);
      sidebar.classList.remove(cfg.disabledClass);

      instance.state = 'bottom';
    },

    /**
     * Disable sticky behavior.
     */
    disableInstance: function (instance, cfg) {
      if (!instance || !instance.sidebar) {
        return;
      }

      this.setNormal(instance, cfg);

      instance.enabled = false;
      instance.sidebar.classList.add(cfg.disabledClass);
    },

    /**
     * Reset all sidebars.
     */
    resetAll: function () {
      var cfg = this.getConfig();
      var self = this;

      this.instances.forEach(function (instance) {
        self.setNormal(instance, cfg);
        if (instance.sidebar) {
          instance.sidebar.classList.remove(cfg.disabledClass);
        }
      });
    },

    /**
     * Get page scroll.
     */
    getScrollY: function () {
      return (
        window.pageYOffset ||
        document.documentElement.scrollTop ||
        document.body.scrollTop ||
        0
      );
    },

    /**
     * Document-based rect.
     */
    getDocumentRect: function (element) {
      if (!element) {
        return {
          top: 0,
          left: 0,
          width: 0,
          height: 0
        };
      }

      var rect = element.getBoundingClientRect();

      return {
        top: rect.top + this.getScrollY(),
        left: rect.left + (window.pageXOffset || document.documentElement.scrollLeft || 0),
        width: rect.width || element.offsetWidth,
        height: rect.height || element.offsetHeight
      };
    },

    /**
     * WordPress admin bar offset.
     */
    getAdminBarOffset: function (cfg) {
      var adminBar = document.getElementById('wpadminbar');

      if (!adminBar) {
        return 0;
      }

      var style = window.getComputedStyle(adminBar);

      if (style.display === 'none' || style.visibility === 'hidden') {
        return 0;
      }

      return adminBar.offsetHeight + cfg.adminBarGap;
    },

    /**
     * Header offset detection.
     */
    getHeaderOffset: function (cfg) {
      var header = document.querySelector(cfg.headerSelector);

      if (!header) {
        return cfg.headerGap;
      }

      var style = window.getComputedStyle(header);
      var position = style.position;

      if (position === 'fixed' || position === 'sticky') {
        return header.offsetHeight + cfg.headerGap;
      }

      return cfg.headerGap;
    },

    /**
     * Detect touch device.
     */
    isTouchDevice: function () {
      return (
        'ontouchstart' in window ||
        navigator.maxTouchPoints > 0 ||
        navigator.msMaxTouchPoints > 0
      );
    },

    /**
     * Detect reduced motion.
     */
    prefersReducedMotion: function () {
      if (!window.matchMedia) {
        return false;
      }

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

    /**
     * Detect RTL.
     */
    isRTL: function () {
      return document.documentElement.dir === 'rtl' ||
        document.body.classList.contains('rtl');
    },

    /**
     * Inject small required CSS.
     */
    injectCSS: function () {
      if (document.getElementById('rx-sticky-sidebar-css')) {
        return;
      }

      var style = document.createElement('style');
      style.id = 'rx-sticky-sidebar-css';

      style.textContent = `
        .rx-is-sticky-sidebar {
          box-sizing: border-box;
          will-change: transform;
        }

        .rx-sidebar-stuck {
          backface-visibility: hidden;
        }

        .rx-sidebar-bottom {
          backface-visibility: hidden;
        }

        .rx-sticky-disabled {
          position: static !important;
          top: auto !important;
          left: auto !important;
          right: auto !important;
          bottom: auto !important;
          width: auto !important;
          transform: none !important;
        }

        .rx-sticky-placeholder {
          box-sizing: border-box;
          pointer-events: none;
        }

        @media (max-width: 991px) {
          .rx-is-sticky-sidebar,
          .rx-sidebar-stuck,
          .rx-sidebar-bottom {
            position: static !important;
            top: auto !important;
            left: auto !important;
            right: auto !important;
            width: auto !important;
            transform: none !important;
          }

          .rx-sticky-placeholder {
            display: none !important;
            height: 0 !important;
          }
        }
      `;

      document.head.appendChild(style);
    }
  };

  /**
   * Public API.
   */
  window.RXStickySidebar = {
    init: function () {
      RXStickySidebar.init();
    },
    refresh: function () {
      RXStickySidebar.resetAll();
      RXStickySidebar.collectSidebars();
      RXStickySidebar.refresh();
    },
    destroy: function () {
      RXStickySidebar.resetAll();
      RXStickySidebar.instances = [];
      window.RXStickySidebarLoaded = false;
    },
    getInstances: function () {
      return RXStickySidebar.instances;
    }
  };

  /**
   * Auto init.
   */
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', function () {
      RXStickySidebar.init();
    });
  } else {
    RXStickySidebar.init();
  }
})();

To customize it from your theme, add this before loading the file:

<script>
window.RX_STICKY_SIDEBAR_CONFIG = {
  sidebarSelector: '.rx-sidebar',
  contentSelector: '.rx-content',
  containerSelector: '.rx-layout',
  headerSelector: '.rx-header',
  footerSelector: '.rx-footer',
  minWidth: 992,
  topGap: 24,
  bottomGap: 24,
  headerGap: 16,
  zIndex: 20,
  debug: false
};
</script>

Best enqueue name for RX Theme:

wp_enqueue_script(
    'rx-chunk-023-sticky-sidebar',
    get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-023-sticky-sidebar.js',
    array(),
    RX_THEME_VERSION,
    true
);

Best HTML structure:

<div class="rx-layout">
  <main class="rx-content">
    <!-- article content -->
  </main>

  <aside class="rx-sidebar">
    <!-- widgets / table of contents / related posts -->
  </aside>
</div>

This file is safe for advanced RX Theme use because it does not force sticky on mobile, respects WordPress admin bar, avoids jQuery dependency, recalculates after resize, and keeps sidebar inside the main content container instead of overlapping the footer.

Leave a Reply

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