chunk-007-reading-progress.js

I’ll build this as a strong standalone reading-progress module for RX Theme: top progress bar, circular indicator, section progress, scroll direction classes, CSS injection, accessibility, performance-safe throttling, and WordPress-friendly auto-init.

Yes friend. Create this file:

assets/static-js/static-chunks-js/chunk-007-reading-progress.js

Then paste this full advanced code.

/*!
 * RX Theme - Reading Progress Advanced Module
 * File: assets/static-js/static-chunks-js/chunk-007-reading-progress.js
 * Version: 1.0.0
 *
 * Features:
 * - Top reading progress bar
 * - Optional bottom progress bar
 * - Circular reading progress indicator
 * - Percent text indicator
 * - Article/content-aware progress calculation
 * - Estimated reading time
 * - Remaining reading time
 * - Scroll direction body classes
 * - Reading-started / reading-completed classes
 * - Milestone events: 25%, 50%, 75%, 100%
 * - Lightweight CSS auto-injection
 * - MutationObserver support for dynamic content
 * - ResizeObserver support
 * - requestAnimationFrame performance optimization
 * - Accessibility support
 */

(function () {
  'use strict';

  const RXReadingProgress = {
    config: {
      enabled: true,

      selectors: {
        content: [
          'article',
          '.entry-content',
          '.post-content',
          '.single-content',
          '.rx-content',
          '.rx-article-content',
          '.site-main',
          'main'
        ],
        exclude: [
          '.rx-no-reading-progress',
          '[data-rx-no-reading-progress]'
        ]
      },

      progressBar: {
        enabled: true,
        position: 'top', // top or bottom
        height: 4,
        zIndex: 99999,
        className: 'rx-reading-progress-bar'
      },

      circularProgress: {
        enabled: true,
        showOnDesktop: true,
        showOnMobile: true,
        mobileBreakpoint: 768,
        position: 'right-bottom',
        size: 54,
        strokeWidth: 5,
        className: 'rx-reading-progress-circle',
        showPercentText: true
      },

      readingTime: {
        enabled: true,
        wordsPerMinute: 220,
        showEstimatedTime: true,
        showRemainingTime: true,
        className: 'rx-reading-time-box'
      },

      behavior: {
        hideBeforeScroll: false,
        hideAfterComplete: false,
        smoothBar: true,
        addBodyClasses: true,
        emitCustomEvents: true,
        updateUrlHash: false
      },

      milestones: [25, 50, 75, 100],

      classes: {
        bodyEnabled: 'rx-reading-progress-enabled',
        bodyStarted: 'rx-reading-started',
        bodyCompleted: 'rx-reading-completed',
        scrollingUp: 'rx-scrolling-up',
        scrollingDown: 'rx-scrolling-down',
        contentActive: 'rx-reading-content-active'
      },

      colors: {
        barBackground: 'var(--rx-reading-progress-bg, linear-gradient(90deg, #0ea5e9, #22c55e))',
        barTrack: 'var(--rx-reading-progress-track, transparent)',
        circleTrack: 'var(--rx-reading-circle-track, rgba(15, 23, 42, 0.15))',
        circleProgress: 'var(--rx-reading-circle-progress, #0ea5e9)',
        circleText: 'var(--rx-reading-circle-text, #0f172a)',
        timeBoxBackground: 'var(--rx-reading-time-bg, rgba(255,255,255,0.92))',
        timeBoxText: 'var(--rx-reading-time-text, #0f172a)'
      },

      storage: {
        enabled: true,
        keyPrefix: 'rx_reading_progress_'
      },

      debug: false
    },

    state: {
      initialized: false,
      contentElement: null,
      progressBar: null,
      circleWrap: null,
      circleProgress: null,
      circleText: null,
      readingTimeBox: null,
      ticking: false,
      lastScrollY: 0,
      progress: 0,
      lastProgress: 0,
      started: false,
      completed: false,
      firedMilestones: {},
      estimatedMinutes: 0,
      remainingMinutes: 0,
      resizeObserver: null,
      mutationObserver: null
    },

    init(customConfig) {
      if (this.state.initialized) return;

      this.config = this.deepMerge(this.config, customConfig || {});

      if (!this.config.enabled) return;
      if (this.isExcludedPage()) return;

      this.state.contentElement = this.findContentElement();

      if (!this.state.contentElement) return;

      this.injectStyles();
      this.createProgressBar();
      this.createCircularProgress();
      this.createReadingTimeBox();
      this.calculateReadingTime();
      this.bindEvents();
      this.observeContentChanges();
      this.restoreProgress();

      document.body.classList.add(this.config.classes.bodyEnabled);
      this.state.contentElement.classList.add(this.config.classes.contentActive);

      this.update();

      this.state.initialized = true;

      this.log('RX Reading Progress initialized');
    },

    deepMerge(target, source) {
      const output = Object.assign({}, target);

      if (!source || typeof source !== 'object') {
        return output;
      }

      Object.keys(source).forEach((key) => {
        if (
          source[key] &&
          typeof source[key] === 'object' &&
          !Array.isArray(source[key])
        ) {
          output[key] = this.deepMerge(target[key] || {}, source[key]);
        } else {
          output[key] = source[key];
        }
      });

      return output;
    },

    isExcludedPage() {
      return this.config.selectors.exclude.some((selector) => {
        return document.querySelector(selector);
      });
    },

    findContentElement() {
      for (let i = 0; i < this.config.selectors.content.length; i++) {
        const selector = this.config.selectors.content[i];
        const element = document.querySelector(selector);

        if (element && this.getTextWordCount(element) > 80) {
          return element;
        }
      }

      return document.body;
    },

    createProgressBar() {
      if (!this.config.progressBar.enabled) return;

      const bar = document.createElement('div');
      const inner = document.createElement('div');

      bar.className = `${this.config.progressBar.className} ${this.config.progressBar.className}--${this.config.progressBar.position}`;
      bar.setAttribute('aria-hidden', 'true');

      inner.className = `${this.config.progressBar.className}__inner`;

      bar.appendChild(inner);
      document.body.appendChild(bar);

      this.state.progressBar = inner;
    },

    createCircularProgress() {
      if (!this.config.circularProgress.enabled) return;

      const isMobile = window.innerWidth <= this.config.circularProgress.mobileBreakpoint;

      if (isMobile && !this.config.circularProgress.showOnMobile) return;
      if (!isMobile && !this.config.circularProgress.showOnDesktop) return;

      const size = this.config.circularProgress.size;
      const strokeWidth = this.config.circularProgress.strokeWidth;
      const radius = (size - strokeWidth) / 2;
      const circumference = 2 * Math.PI * radius;

      const wrap = document.createElement('button');
      wrap.type = 'button';
      wrap.className = `${this.config.circularProgress.className} ${this.config.circularProgress.className}--${this.config.circularProgress.position}`;
      wrap.setAttribute('aria-label', 'Reading progress. Click to go to top.');
      wrap.setAttribute('title', 'Reading progress - click to go to top');

      wrap.innerHTML = `
        <svg class="${this.config.circularProgress.className}__svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" aria-hidden="true">
          <circle
            class="${this.config.circularProgress.className}__track"
            cx="${size / 2}"
            cy="${size / 2}"
            r="${radius}"
            stroke-width="${strokeWidth}"
            fill="none"
          ></circle>
          <circle
            class="${this.config.circularProgress.className}__progress"
            cx="${size / 2}"
            cy="${size / 2}"
            r="${radius}"
            stroke-width="${strokeWidth}"
            fill="none"
            stroke-linecap="round"
            stroke-dasharray="${circumference}"
            stroke-dashoffset="${circumference}"
          ></circle>
        </svg>
        ${
          this.config.circularProgress.showPercentText
            ? `<span class="${this.config.circularProgress.className}__text">0%</span>`
            : ''
        }
      `;

      wrap.addEventListener('click', () => {
        window.scrollTo({
          top: 0,
          behavior: 'smooth'
        });
      });

      document.body.appendChild(wrap);

      this.state.circleWrap = wrap;
      this.state.circleProgress = wrap.querySelector(`.${this.config.circularProgress.className}__progress`);
      this.state.circleText = wrap.querySelector(`.${this.config.circularProgress.className}__text`);
    },

    createReadingTimeBox() {
      if (!this.config.readingTime.enabled) return;

      const box = document.createElement('div');
      box.className = this.config.readingTime.className;
      box.setAttribute('aria-live', 'polite');

      box.innerHTML = `
        <span class="${this.config.readingTime.className}__estimated"></span>
        <span class="${this.config.readingTime.className}__remaining"></span>
      `;

      const target = this.state.contentElement;

      if (target && target.parentNode) {
        target.parentNode.insertBefore(box, target);
      } else {
        document.body.appendChild(box);
      }

      this.state.readingTimeBox = box;
    },

    calculateReadingTime() {
      const words = this.getTextWordCount(this.state.contentElement);
      const minutes = Math.max(1, Math.ceil(words / this.config.readingTime.wordsPerMinute));

      this.state.estimatedMinutes = minutes;
      this.state.remainingMinutes = minutes;

      this.updateReadingTimeText();
    },

    getTextWordCount(element) {
      if (!element) return 0;

      const clone = element.cloneNode(true);

      clone.querySelectorAll('script, style, noscript, iframe, svg, canvas, form').forEach((node) => {
        node.remove();
      });

      const text = clone.textContent || '';
      const cleanText = text.trim().replace(/\s+/g, ' ');

      if (!cleanText) return 0;

      return cleanText.split(' ').length;
    },

    getProgressData() {
      const element = this.state.contentElement;

      if (!element) {
        return {
          progress: 0,
          start: 0,
          end: 0,
          scrollY: window.scrollY || window.pageYOffset
        };
      }

      const rect = element.getBoundingClientRect();
      const scrollY = window.scrollY || window.pageYOffset;
      const viewportHeight = window.innerHeight || document.documentElement.clientHeight;

      const contentTop = rect.top + scrollY;
      const contentHeight = element.offsetHeight;
      const contentBottom = contentTop + contentHeight;

      const start = Math.max(0, contentTop - viewportHeight * 0.15);
      const end = Math.max(start + 1, contentBottom - viewportHeight * 0.85);

      const rawProgress = ((scrollY - start) / (end - start)) * 100;
      const progress = Math.min(100, Math.max(0, rawProgress));

      return {
        progress,
        start,
        end,
        scrollY,
        contentTop,
        contentBottom,
        viewportHeight,
        contentHeight
      };
    },

    update() {
      const data = this.getProgressData();
      const progress = data.progress;

      this.state.lastProgress = this.state.progress;
      this.state.progress = progress;

      this.updateProgressBar(progress);
      this.updateCircularProgress(progress);
      this.updateReadingState(progress);
      this.updateScrollDirection(data.scrollY);
      this.updateRemainingTime(progress);
      this.handleMilestones(progress);
      this.storeProgress(progress);

      this.state.ticking = false;
    },

    requestUpdate() {
      if (this.state.ticking) return;

      this.state.ticking = true;

      window.requestAnimationFrame(() => {
        this.update();
      });
    },

    updateProgressBar(progress) {
      if (!this.state.progressBar) return;

      const value = `${progress.toFixed(2)}%`;

      if (this.config.behavior.smoothBar) {
        this.state.progressBar.style.transform = `scaleX(${progress / 100})`;
      } else {
        this.state.progressBar.style.width = value;
      }

      this.state.progressBar.setAttribute('data-progress', Math.round(progress));
    },

    updateCircularProgress(progress) {
      if (!this.state.circleProgress) return;

      const radius = this.state.circleProgress.r.baseVal.value;
      const circumference = 2 * Math.PI * radius;
      const offset = circumference - (progress / 100) * circumference;

      this.state.circleProgress.style.strokeDashoffset = offset;

      if (this.state.circleText) {
        this.state.circleText.textContent = `${Math.round(progress)}%`;
      }

      if (this.state.circleWrap) {
        this.state.circleWrap.setAttribute('aria-label', `Reading progress ${Math.round(progress)} percent. Click to go to top.`);
      }
    },

    updateReadingState(progress) {
      if (!this.config.behavior.addBodyClasses) return;

      if (progress > 2 && !this.state.started) {
        this.state.started = true;
        document.body.classList.add(this.config.classes.bodyStarted);
        this.emit('rx:reading-started', { progress });
      }

      if (progress >= 98 && !this.state.completed) {
        this.state.completed = true;
        document.body.classList.add(this.config.classes.bodyCompleted);
        this.emit('rx:reading-completed', { progress: 100 });

        if (this.config.behavior.hideAfterComplete) {
          this.hideIndicators();
        }
      }

      if (progress < 95 && this.state.completed) {
        this.state.completed = false;
        document.body.classList.remove(this.config.classes.bodyCompleted);
      }
    },

    updateScrollDirection(currentScrollY) {
      if (!this.config.behavior.addBodyClasses) return;

      const lastScrollY = this.state.lastScrollY;

      if (currentScrollY > lastScrollY + 5) {
        document.body.classList.add(this.config.classes.scrollingDown);
        document.body.classList.remove(this.config.classes.scrollingUp);
      }

      if (currentScrollY < lastScrollY - 5) {
        document.body.classList.add(this.config.classes.scrollingUp);
        document.body.classList.remove(this.config.classes.scrollingDown);
      }

      this.state.lastScrollY = currentScrollY;
    },

    updateRemainingTime(progress) {
      const remainingRatio = Math.max(0, (100 - progress) / 100);
      this.state.remainingMinutes = Math.max(0, Math.ceil(this.state.estimatedMinutes * remainingRatio));

      this.updateReadingTimeText();
    },

    updateReadingTimeText() {
      if (!this.state.readingTimeBox) return;

      const estimated = this.state.readingTimeBox.querySelector(`.${this.config.readingTime.className}__estimated`);
      const remaining = this.state.readingTimeBox.querySelector(`.${this.config.readingTime.className}__remaining`);

      if (estimated && this.config.readingTime.showEstimatedTime) {
        estimated.textContent = `${this.state.estimatedMinutes} min read`;
      }

      if (remaining && this.config.readingTime.showRemainingTime) {
        const min = this.state.remainingMinutes;

        if (min <= 0) {
          remaining.textContent = 'Completed';
        } else {
          remaining.textContent = `${min} min left`;
        }
      }
    },

    handleMilestones(progress) {
      this.config.milestones.forEach((milestone) => {
        if (progress >= milestone && !this.state.firedMilestones[milestone]) {
          this.state.firedMilestones[milestone] = true;

          this.emit('rx:reading-milestone', {
            milestone,
            progress
          });

          document.body.setAttribute('data-rx-reading-milestone', String(milestone));

          this.log(`Reading milestone reached: ${milestone}%`);
        }
      });
    },

    emit(eventName, detail) {
      if (!this.config.behavior.emitCustomEvents) return;

      const event = new CustomEvent(eventName, {
        bubbles: true,
        detail: detail || {}
      });

      document.dispatchEvent(event);
    },

    bindEvents() {
      window.addEventListener('scroll', this.requestUpdate.bind(this), {
        passive: true
      });

      window.addEventListener('resize', this.debounce(() => {
        this.calculateReadingTime();
        this.requestUpdate();
      }, 150), {
        passive: true
      });

      document.addEventListener('visibilitychange', () => {
        if (!document.hidden) {
          this.requestUpdate();
        }
      });

      window.addEventListener('load', () => {
        this.calculateReadingTime();
        this.requestUpdate();
      });
    },

    observeContentChanges() {
      const element = this.state.contentElement;

      if (!element) return;

      if ('ResizeObserver' in window) {
        this.state.resizeObserver = new ResizeObserver(
          this.debounce(() => {
            this.calculateReadingTime();
            this.requestUpdate();
          }, 150)
        );

        this.state.resizeObserver.observe(element);
      }

      if ('MutationObserver' in window) {
        this.state.mutationObserver = new MutationObserver(
          this.debounce(() => {
            this.calculateReadingTime();
            this.requestUpdate();
          }, 300)
        );

        this.state.mutationObserver.observe(element, {
          childList: true,
          subtree: true,
          characterData: true
        });
      }
    },

    hideIndicators() {
      if (this.state.progressBar && this.state.progressBar.parentNode) {
        this.state.progressBar.parentNode.classList.add('rx-reading-hidden');
      }

      if (this.state.circleWrap) {
        this.state.circleWrap.classList.add('rx-reading-hidden');
      }
    },

    showIndicators() {
      if (this.state.progressBar && this.state.progressBar.parentNode) {
        this.state.progressBar.parentNode.classList.remove('rx-reading-hidden');
      }

      if (this.state.circleWrap) {
        this.state.circleWrap.classList.remove('rx-reading-hidden');
      }
    },

    getStorageKey() {
      const path = window.location.pathname || 'home';
      return `${this.config.storage.keyPrefix}${path}`;
    },

    storeProgress(progress) {
      if (!this.config.storage.enabled) return;

      try {
        const data = {
          progress: Math.round(progress),
          url: window.location.href,
          updatedAt: Date.now()
        };

        localStorage.setItem(this.getStorageKey(), JSON.stringify(data));
      } catch (error) {
        this.log('Storage failed', error);
      }
    },

    restoreProgress() {
      if (!this.config.storage.enabled) return;

      try {
        const saved = localStorage.getItem(this.getStorageKey());

        if (!saved) return;

        const data = JSON.parse(saved);

        if (!data || typeof data.progress === 'undefined') return;

        document.body.setAttribute('data-rx-saved-reading-progress', String(data.progress));
      } catch (error) {
        this.log('Restore progress failed', error);
      }
    },

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

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

        clearTimeout(timer);

        timer = setTimeout(() => {
          fn.apply(context, args);
        }, delay);
      };
    },

    injectStyles() {
      if (document.getElementById('rx-reading-progress-style')) return;

      const style = document.createElement('style');
      style.id = 'rx-reading-progress-style';

      const barPosition = this.config.progressBar.position === 'bottom'
        ? 'bottom: 0; top: auto;'
        : 'top: 0; bottom: auto;';

      style.textContent = `
        .${this.config.progressBar.className} {
          position: fixed;
          left: 0;
          right: 0;
          ${barPosition}
          width: 100%;
          height: ${this.config.progressBar.height}px;
          background: ${this.config.colors.barTrack};
          z-index: ${this.config.progressBar.zIndex};
          pointer-events: none;
          overflow: hidden;
        }

        .${this.config.progressBar.className}__inner {
          display: block;
          width: 100%;
          height: 100%;
          background: ${this.config.colors.barBackground};
          transform: scaleX(0);
          transform-origin: left center;
          transition: transform 120ms linear;
          will-change: transform;
        }

        .${this.config.circularProgress.className} {
          position: fixed;
          width: ${this.config.circularProgress.size}px;
          height: ${this.config.circularProgress.size}px;
          border: 0;
          border-radius: 999px;
          background: rgba(255, 255, 255, 0.92);
          box-shadow: 0 10px 30px rgba(15, 23, 42, 0.16);
          cursor: pointer;
          z-index: ${this.config.progressBar.zIndex - 1};
          display: inline-flex;
          align-items: center;
          justify-content: center;
          padding: 0;
          backdrop-filter: blur(10px);
          -webkit-backdrop-filter: blur(10px);
          transition: opacity 180ms ease, transform 180ms ease, box-shadow 180ms ease;
        }

        .${this.config.circularProgress.className}:hover {
          transform: translateY(-2px);
          box-shadow: 0 14px 36px rgba(15, 23, 42, 0.22);
        }

        .${this.config.circularProgress.className}:focus-visible {
          outline: 3px solid rgba(14, 165, 233, 0.35);
          outline-offset: 3px;
        }

        .${this.config.circularProgress.className}--right-bottom {
          right: 22px;
          bottom: 22px;
        }

        .${this.config.circularProgress.className}--left-bottom {
          left: 22px;
          bottom: 22px;
        }

        .${this.config.circularProgress.className}--right-middle {
          right: 22px;
          top: 50%;
          transform: translateY(-50%);
        }

        .${this.config.circularProgress.className}--left-middle {
          left: 22px;
          top: 50%;
          transform: translateY(-50%);
        }

        .${this.config.circularProgress.className}__svg {
          position: absolute;
          inset: 0;
          transform: rotate(-90deg);
        }

        .${this.config.circularProgress.className}__track {
          stroke: ${this.config.colors.circleTrack};
        }

        .${this.config.circularProgress.className}__progress {
          stroke: ${this.config.colors.circleProgress};
          transition: stroke-dashoffset 120ms linear;
          will-change: stroke-dashoffset;
        }

        .${this.config.circularProgress.className}__text {
          position: relative;
          z-index: 2;
          font-size: 12px;
          font-weight: 700;
          line-height: 1;
          color: ${this.config.colors.circleText};
          font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        }

        .${this.config.readingTime.className} {
          display: inline-flex;
          align-items: center;
          gap: 10px;
          margin: 0 0 18px;
          padding: 8px 12px;
          border-radius: 999px;
          background: ${this.config.colors.timeBoxBackground};
          color: ${this.config.colors.timeBoxText};
          box-shadow: 0 4px 18px rgba(15, 23, 42, 0.08);
          font-size: 14px;
          font-weight: 600;
          line-height: 1.3;
        }

        .${this.config.readingTime.className}__estimated::before {
          content: "⏱ ";
        }

        .${this.config.readingTime.className}__remaining::before {
          content: "• ";
          opacity: 0.65;
        }

        .rx-reading-hidden {
          opacity: 0 !important;
          pointer-events: none !important;
          transform: translateY(8px);
        }

        body.rx-reading-started .${this.config.progressBar.className} {
          opacity: 1;
        }

        @media (max-width: ${this.config.circularProgress.mobileBreakpoint}px) {
          .${this.config.circularProgress.className} {
            width: 48px;
            height: 48px;
            right: 14px;
            bottom: 14px;
          }

          .${this.config.circularProgress.className}__text {
            font-size: 11px;
          }

          .${this.config.readingTime.className} {
            font-size: 13px;
            padding: 7px 10px;
          }
        }

        @media (prefers-reduced-motion: reduce) {
          .${this.config.progressBar.className}__inner,
          .${this.config.circularProgress.className},
          .${this.config.circularProgress.className}__progress {
            transition: none !important;
          }
        }

        @media print {
          .${this.config.progressBar.className},
          .${this.config.circularProgress.className},
          .${this.config.readingTime.className} {
            display: none !important;
          }
        }
      `;

      document.head.appendChild(style);
    },

    destroy() {
      window.removeEventListener('scroll', this.requestUpdate);

      if (this.state.progressBar && this.state.progressBar.parentNode) {
        this.state.progressBar.parentNode.remove();
      }

      if (this.state.circleWrap) {
        this.state.circleWrap.remove();
      }

      if (this.state.readingTimeBox) {
        this.state.readingTimeBox.remove();
      }

      if (this.state.resizeObserver) {
        this.state.resizeObserver.disconnect();
      }

      if (this.state.mutationObserver) {
        this.state.mutationObserver.disconnect();
      }

      document.body.classList.remove(
        this.config.classes.bodyEnabled,
        this.config.classes.bodyStarted,
        this.config.classes.bodyCompleted,
        this.config.classes.scrollingUp,
        this.config.classes.scrollingDown
      );

      this.state.initialized = false;
    },

    log() {
      if (!this.config.debug) return;
      console.log.apply(console, ['[RXReadingProgress]', ...arguments]);
    }
  };

  window.RXReadingProgress = RXReadingProgress;

  function autoInit() {
    const customConfig = window.rxReadingProgressConfig || {};
    RXReadingProgress.init(customConfig);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', autoInit);
  } else {
    autoInit();
  }
})();

To enqueue this file in your RX Theme, add this in your theme enqueue file, for example:

inc/core/enqueue.php

<?php
/**
 * RX Theme Reading Progress JS
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

function rx_theme_enqueue_reading_progress_script() {

	if ( is_admin() ) {
		return;
	}

	if ( ! is_singular() ) {
		return;
	}

	$theme_version = wp_get_theme()->get( 'Version' );

	wp_enqueue_script(
		'rx-reading-progress',
		get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-007-reading-progress.js',
		array(),
		$theme_version,
		true
	);

	wp_localize_script(
		'rx-reading-progress',
		'rxReadingProgressConfig',
		array(
			'enabled' => true,

			'progressBar' => array(
				'enabled'  => true,
				'position' => 'top',
				'height'   => 4,
			),

			'circularProgress' => array(
				'enabled'         => true,
				'showOnDesktop'   => true,
				'showOnMobile'    => true,
				'position'        => 'right-bottom',
				'size'            => 54,
				'strokeWidth'     => 5,
				'showPercentText' => true,
			),

			'readingTime' => array(
				'enabled'           => true,
				'wordsPerMinute'    => 220,
				'showEstimatedTime' => true,
				'showRemainingTime' => true,
			),

			'behavior' => array(
				'hideBeforeScroll'  => false,
				'hideAfterComplete' => false,
				'smoothBar'         => true,
				'addBodyClasses'    => true,
				'emitCustomEvents'  => true,
			),
		)
	);
}
add_action( 'wp_enqueue_scripts', 'rx_theme_enqueue_reading_progress_script' );

Best use for RX Theme:

assets/
└── static-js/
    └── static-chunks-js/
        └── chunk-007-reading-progress.js

This file is safe as a theme chunk because it is independent, auto-initialized, and only loads on single posts/pages when you enqueue it with is_singular().

Leave a Reply

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