I’ll build this as a complete single-post.js for your RX Theme, focused on useful single-article features without needing external libraries. It will be safe vanilla JavaScript, modular, and easy to enable from your theme.
My dear friend, create this file:
rx-theme/inc/js/single-post.js
Then paste the full code below.
/*!
* RX Theme - Single Post JavaScript
* File: inc/js/single-post.js
* Author: RxHarun
* Description:
* Advanced single post UX features for RX Theme:
* - Reading progress bar
* - Estimated reading time
* - Table of contents
* - Smooth anchor scrolling
* - Active heading highlight
* - Back to top button
* - Copy current post link
* - Native share button
* - Copy code button
* - External link safety
* - Image lightbox
* - Lazy iframe loading
* - Responsive video wrapper
* - Footnote back links
* - Print article button
* - Font size controls
* - Dark reading mode toggle
* - Sticky article tools
* - Selection share helper
* - Comment form helper
* - Heading anchor links
*/
(function () {
'use strict';
const RXSinglePost = {
config: {
contentSelector: '.entry-content, .post-content, article .content, article',
articleSelector: 'article',
tocContainerSelector: '#rx-table-of-contents',
tocListSelector: '#rx-toc-list',
progressBarId: 'rx-reading-progress-bar',
backToTopId: 'rx-back-to-top',
minHeadingsForToc: 3,
headingSelector: 'h2, h3',
scrollOffset: 90,
readingWordsPerMinute: 220,
storagePrefix: 'rx_theme_single_',
},
state: {
content: null,
article: null,
headings: [],
ticking: false,
activeHeadingId: null,
selectionPopup: null,
},
init() {
this.state.content = document.querySelector(this.config.contentSelector);
this.state.article = document.querySelector(this.config.articleSelector);
if (!this.state.content) {
return;
}
this.createReadingProgressBar();
this.createReadingTime();
this.createTableOfContents();
this.createHeadingAnchors();
this.enableSmoothScrolling();
this.enableActiveTocHighlight();
this.createBackToTopButton();
this.enableCopyCurrentLink();
this.enableNativeShare();
this.enableCopyCodeButtons();
this.fixExternalLinks();
this.enableImageLightbox();
this.lazyLoadIframes();
this.wrapResponsiveVideos();
this.improveTables();
this.enableFootnoteBacklinks();
this.createPrintButton();
this.createFontSizeControls();
this.createDarkReadingMode();
this.createStickyTools();
this.createSelectionShare();
this.improveCommentForm();
this.restoreReadingPosition();
this.saveReadingPosition();
this.addKeyboardShortcuts();
window.addEventListener('scroll', () => this.onScroll(), { passive: true });
window.addEventListener('resize', () => this.onResize(), { passive: true });
},
qs(selector, parent = document) {
return parent.querySelector(selector);
},
qsa(selector, parent = document) {
return Array.prototype.slice.call(parent.querySelectorAll(selector));
},
createEl(tag, className, text) {
const el = document.createElement(tag);
if (className) {
el.className = className;
}
if (text) {
el.textContent = text;
}
return el;
},
slugify(text) {
return text
.toString()
.toLowerCase()
.trim()
.replace(/&/g, 'and')
.replace(/[^a-z0-9\u0980-\u09FF]+/g, '-')
.replace(/^-+|-+$/g, '');
},
uniqueId(base) {
let id = base || 'rx-heading';
let counter = 1;
while (document.getElementById(id)) {
id = `${base}-${counter}`;
counter += 1;
}
return id;
},
createReadingProgressBar() {
if (document.getElementById(this.config.progressBarId)) {
return;
}
const bar = this.createEl('div', 'rx-reading-progress-bar');
bar.id = this.config.progressBarId;
bar.setAttribute('aria-hidden', 'true');
document.body.appendChild(bar);
},
updateReadingProgress() {
const bar = document.getElementById(this.config.progressBarId);
if (!bar || !this.state.article) {
return;
}
const articleRect = this.state.article.getBoundingClientRect();
const articleTop = window.scrollY + articleRect.top;
const articleHeight = this.state.article.offsetHeight;
const windowHeight = window.innerHeight;
const scrollPosition = window.scrollY - articleTop;
const readableHeight = Math.max(articleHeight - windowHeight, 1);
const progress = Math.min(Math.max(scrollPosition / readableHeight, 0), 1);
bar.style.transform = `scaleX(${progress})`;
},
createReadingTime() {
const existing = document.querySelector('.rx-reading-time');
if (existing) {
return;
}
const text = this.state.content.innerText || '';
const words = text.trim().split(/\s+/).filter(Boolean).length;
const minutes = Math.max(1, Math.ceil(words / this.config.readingWordsPerMinute));
const readingTime = this.createEl(
'div',
'rx-reading-time',
`${minutes} min read`
);
readingTime.setAttribute('aria-label', `Estimated reading time ${minutes} minutes`);
const title = document.querySelector('.entry-title, .post-title, h1');
if (title && title.parentNode) {
title.parentNode.insertBefore(readingTime, title.nextSibling);
} else {
this.state.content.insertBefore(readingTime, this.state.content.firstChild);
}
},
createTableOfContents() {
this.state.headings = this.qsa(this.config.headingSelector, this.state.content)
.filter((heading) => heading.textContent.trim().length > 0);
if (this.state.headings.length < this.config.minHeadingsForToc) {
return;
}
this.state.headings.forEach((heading) => {
if (!heading.id) {
const base = this.slugify(heading.textContent) || 'rx-heading';
heading.id = this.uniqueId(base);
}
});
let tocContainer = document.querySelector(this.config.tocContainerSelector);
if (!tocContainer) {
tocContainer = this.createEl('nav', 'rx-table-of-contents');
tocContainer.id = 'rx-table-of-contents';
tocContainer.setAttribute('aria-label', 'Table of contents');
const tocTitle = this.createEl('button', 'rx-toc-title', 'Table of Contents');
tocTitle.type = 'button';
tocTitle.setAttribute('aria-expanded', 'true');
const tocList = this.createEl('ol', 'rx-toc-list');
tocList.id = 'rx-toc-list';
tocContainer.appendChild(tocTitle);
tocContainer.appendChild(tocList);
const firstParagraph = this.qs('p', this.state.content);
if (firstParagraph && firstParagraph.parentNode) {
firstParagraph.parentNode.insertBefore(tocContainer, firstParagraph);
} else {
this.state.content.insertBefore(tocContainer, this.state.content.firstChild);
}
tocTitle.addEventListener('click', () => {
const expanded = tocTitle.getAttribute('aria-expanded') === 'true';
tocTitle.setAttribute('aria-expanded', expanded ? 'false' : 'true');
tocContainer.classList.toggle('is-collapsed', expanded);
});
}
const tocList = tocContainer.querySelector('ol') || this.createEl('ol', 'rx-toc-list');
tocList.innerHTML = '';
this.state.headings.forEach((heading) => {
const level = heading.tagName.toLowerCase() === 'h3' ? 'rx-toc-level-3' : 'rx-toc-level-2';
const li = this.createEl('li', `rx-toc-item ${level}`);
const link = this.createEl('a', 'rx-toc-link', heading.textContent.trim());
link.href = `#${heading.id}`;
link.dataset.target = heading.id;
li.appendChild(link);
tocList.appendChild(li);
});
if (!tocList.parentNode) {
tocContainer.appendChild(tocList);
}
},
createHeadingAnchors() {
const headings = this.qsa('h2, h3, h4, h5, h6', this.state.content);
headings.forEach((heading) => {
if (!heading.id) {
const base = this.slugify(heading.textContent) || 'rx-heading';
heading.id = this.uniqueId(base);
}
if (heading.querySelector('.rx-heading-anchor')) {
return;
}
const anchor = this.createEl('a', 'rx-heading-anchor', '#');
anchor.href = `#${heading.id}`;
anchor.setAttribute('aria-label', `Link to ${heading.textContent.trim()}`);
anchor.title = 'Copy heading link';
anchor.addEventListener('click', (event) => {
event.preventDefault();
const url = `${window.location.origin}${window.location.pathname}#${heading.id}`;
this.copyText(url);
this.showToast('Heading link copied');
history.replaceState(null, '', `#${heading.id}`);
});
heading.appendChild(anchor);
});
},
enableSmoothScrolling() {
document.addEventListener('click', (event) => {
const link = event.target.closest('a[href^="#"]');
if (!link) {
return;
}
const hash = link.getAttribute('href');
if (!hash || hash === '#') {
return;
}
const target = document.getElementById(hash.substring(1));
if (!target) {
return;
}
event.preventDefault();
this.scrollToElement(target);
history.pushState(null, '', hash);
});
},
scrollToElement(element) {
const top =
window.scrollY +
element.getBoundingClientRect().top -
this.config.scrollOffset;
window.scrollTo({
top,
behavior: 'smooth',
});
},
enableActiveTocHighlight() {
if (!('IntersectionObserver' in window)) {
return;
}
const tocLinks = this.qsa('.rx-toc-link');
if (!tocLinks.length || !this.state.headings.length) {
return;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.setActiveToc(entry.target.id);
}
});
},
{
rootMargin: '-20% 0px -70% 0px',
threshold: 0,
}
);
this.state.headings.forEach((heading) => observer.observe(heading));
},
setActiveToc(id) {
if (!id || this.state.activeHeadingId === id) {
return;
}
this.state.activeHeadingId = id;
this.qsa('.rx-toc-link').forEach((link) => {
link.classList.toggle('is-active', link.dataset.target === id);
});
},
createBackToTopButton() {
if (document.getElementById(this.config.backToTopId)) {
return;
}
const button = this.createEl('button', 'rx-back-to-top', '↑');
button.id = this.config.backToTopId;
button.type = 'button';
button.setAttribute('aria-label', 'Back to top');
button.title = 'Back to top';
button.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
});
document.body.appendChild(button);
},
updateBackToTopButton() {
const button = document.getElementById(this.config.backToTopId);
if (!button) {
return;
}
button.classList.toggle('is-visible', window.scrollY > 500);
},
enableCopyCurrentLink() {
document.addEventListener('click', (event) => {
const button = event.target.closest('[data-rx-copy-link]');
if (!button) {
return;
}
event.preventDefault();
this.copyText(window.location.href);
this.showToast('Post link copied');
});
},
enableNativeShare() {
document.addEventListener('click', (event) => {
const button = event.target.closest('[data-rx-share]');
if (!button) {
return;
}
event.preventDefault();
const shareData = {
title: document.title,
text: this.getMetaDescription(),
url: window.location.href,
};
if (navigator.share) {
navigator.share(shareData).catch(() => {
this.copyText(window.location.href);
this.showToast('Link copied');
});
} else {
this.copyText(window.location.href);
this.showToast('Link copied');
}
});
},
getMetaDescription() {
const meta = document.querySelector('meta[name="description"]');
return meta ? meta.getAttribute('content') : '';
},
enableCopyCodeButtons() {
const codeBlocks = this.qsa('pre');
codeBlocks.forEach((pre) => {
if (pre.classList.contains('rx-code-ready')) {
return;
}
pre.classList.add('rx-code-ready');
const button = this.createEl('button', 'rx-copy-code-button', 'Copy');
button.type = 'button';
button.setAttribute('aria-label', 'Copy code');
button.addEventListener('click', () => {
const code = pre.querySelector('code') || pre;
this.copyText(code.innerText);
button.textContent = 'Copied';
window.setTimeout(() => {
button.textContent = 'Copy';
}, 1800);
});
pre.appendChild(button);
});
},
fixExternalLinks() {
const links = this.qsa('a[href]', this.state.content);
const currentHost = window.location.hostname;
links.forEach((link) => {
try {
const url = new URL(link.href);
if (url.hostname && url.hostname !== currentHost) {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'nofollow noopener noreferrer external');
link.classList.add('rx-external-link');
if (!link.getAttribute('aria-label')) {
link.setAttribute(
'aria-label',
`${link.textContent.trim()} opens in a new tab`
);
}
}
} catch (error) {
// Invalid URL, ignore safely.
}
});
},
enableImageLightbox() {
const images = this.qsa('img', this.state.content);
if (!images.length) {
return;
}
let overlay = document.querySelector('.rx-lightbox-overlay');
if (!overlay) {
overlay = this.createEl('div', 'rx-lightbox-overlay');
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-hidden', 'true');
const close = this.createEl('button', 'rx-lightbox-close', '×');
close.type = 'button';
close.setAttribute('aria-label', 'Close image preview');
const img = document.createElement('img');
img.className = 'rx-lightbox-image';
img.alt = '';
const caption = this.createEl('div', 'rx-lightbox-caption');
overlay.appendChild(close);
overlay.appendChild(img);
overlay.appendChild(caption);
document.body.appendChild(overlay);
close.addEventListener('click', () => this.closeLightbox());
overlay.addEventListener('click', (event) => {
if (event.target === overlay) {
this.closeLightbox();
}
});
}
images.forEach((image) => {
if (image.closest('a')) {
return;
}
image.classList.add('rx-clickable-image');
image.setAttribute('tabindex', '0');
image.setAttribute('role', 'button');
image.setAttribute('aria-label', 'Open image preview');
image.addEventListener('click', () => this.openLightbox(image));
image.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.openLightbox(image);
}
});
});
},
openLightbox(image) {
const overlay = document.querySelector('.rx-lightbox-overlay');
if (!overlay) {
return;
}
const preview = overlay.querySelector('.rx-lightbox-image');
const caption = overlay.querySelector('.rx-lightbox-caption');
preview.src = image.currentSrc || image.src;
preview.alt = image.alt || '';
caption.textContent =
image.getAttribute('data-caption') ||
image.alt ||
image.closest('figure')?.querySelector('figcaption')?.textContent ||
'';
overlay.setAttribute('aria-hidden', 'false');
overlay.classList.add('is-open');
document.body.classList.add('rx-lightbox-open');
},
closeLightbox() {
const overlay = document.querySelector('.rx-lightbox-overlay');
if (!overlay) {
return;
}
overlay.setAttribute('aria-hidden', 'true');
overlay.classList.remove('is-open');
document.body.classList.remove('rx-lightbox-open');
},
lazyLoadIframes() {
const iframes = this.qsa('iframe', this.state.content);
iframes.forEach((iframe) => {
iframe.loading = 'lazy';
if (!iframe.getAttribute('title')) {
iframe.setAttribute('title', 'Embedded content');
}
});
},
wrapResponsiveVideos() {
const selectors = [
'iframe[src*="youtube.com"]',
'iframe[src*="youtu.be"]',
'iframe[src*="vimeo.com"]',
'iframe[src*="dailymotion.com"]',
];
const videos = this.qsa(selectors.join(','), this.state.content);
videos.forEach((video) => {
if (video.parentElement && video.parentElement.classList.contains('rx-video-wrapper')) {
return;
}
const wrapper = this.createEl('div', 'rx-video-wrapper');
video.parentNode.insertBefore(wrapper, video);
wrapper.appendChild(video);
});
},
improveTables() {
const tables = this.qsa('table', this.state.content);
tables.forEach((table) => {
if (table.parentElement && table.parentElement.classList.contains('rx-table-wrapper')) {
return;
}
const wrapper = this.createEl('div', 'rx-table-wrapper');
wrapper.setAttribute('tabindex', '0');
wrapper.setAttribute('role', 'region');
wrapper.setAttribute('aria-label', 'Scrollable table');
table.parentNode.insertBefore(wrapper, table);
wrapper.appendChild(table);
});
},
enableFootnoteBacklinks() {
const footnotes = this.qsa('a[href^="#fn"], a[href^="#footnote"]', this.state.content);
footnotes.forEach((link) => {
const targetId = link.getAttribute('href')?.substring(1);
const target = targetId ? document.getElementById(targetId) : null;
if (!target) {
return;
}
const backlinkId = link.id || this.uniqueId('rx-footnote-ref');
link.id = backlinkId;
if (!target.querySelector('.rx-footnote-back')) {
const back = this.createEl('a', 'rx-footnote-back', '↩ Back');
back.href = `#${backlinkId}`;
target.appendChild(document.createTextNode(' '));
target.appendChild(back);
}
});
},
createPrintButton() {
if (document.querySelector('[data-rx-print-created]')) {
return;
}
const button = this.createEl('button', 'rx-print-button', 'Print');
button.type = 'button';
button.setAttribute('data-rx-print-created', 'true');
button.setAttribute('aria-label', 'Print this article');
button.addEventListener('click', () => {
window.print();
});
this.insertIntoPostTools(button);
},
createFontSizeControls() {
if (document.querySelector('.rx-font-size-controls')) {
return;
}
const wrapper = this.createEl('div', 'rx-font-size-controls');
const decrease = this.createEl('button', 'rx-font-size-button', 'A−');
const reset = this.createEl('button', 'rx-font-size-button', 'A');
const increase = this.createEl('button', 'rx-font-size-button', 'A+');
decrease.type = 'button';
reset.type = 'button';
increase.type = 'button';
decrease.setAttribute('aria-label', 'Decrease article font size');
reset.setAttribute('aria-label', 'Reset article font size');
increase.setAttribute('aria-label', 'Increase article font size');
const applyFontSize = (size) => {
this.state.content.style.fontSize = size ? `${size}px` : '';
if (size) {
localStorage.setItem(`${this.config.storagePrefix}font_size`, String(size));
} else {
localStorage.removeItem(`${this.config.storagePrefix}font_size`);
}
};
const currentSaved = localStorage.getItem(`${this.config.storagePrefix}font_size`);
if (currentSaved) {
applyFontSize(parseInt(currentSaved, 10));
}
decrease.addEventListener('click', () => {
const current = parseInt(window.getComputedStyle(this.state.content).fontSize, 10);
applyFontSize(Math.max(current - 1, 14));
});
reset.addEventListener('click', () => {
applyFontSize(null);
});
increase.addEventListener('click', () => {
const current = parseInt(window.getComputedStyle(this.state.content).fontSize, 10);
applyFontSize(Math.min(current + 1, 24));
});
wrapper.appendChild(decrease);
wrapper.appendChild(reset);
wrapper.appendChild(increase);
this.insertIntoPostTools(wrapper);
},
createDarkReadingMode() {
if (document.querySelector('[data-rx-reading-mode]')) {
return;
}
const button = this.createEl('button', 'rx-reading-mode-button', 'Reading Mode');
button.type = 'button';
button.setAttribute('data-rx-reading-mode', 'true');
button.setAttribute('aria-pressed', 'false');
const saved = localStorage.getItem(`${this.config.storagePrefix}reading_mode`);
if (saved === 'dark') {
document.body.classList.add('rx-dark-reading-mode');
button.setAttribute('aria-pressed', 'true');
}
button.addEventListener('click', () => {
const enabled = document.body.classList.toggle('rx-dark-reading-mode');
button.setAttribute('aria-pressed', enabled ? 'true' : 'false');
if (enabled) {
localStorage.setItem(`${this.config.storagePrefix}reading_mode`, 'dark');
} else {
localStorage.removeItem(`${this.config.storagePrefix}reading_mode`);
}
});
this.insertIntoPostTools(button);
},
createStickyTools() {
if (document.querySelector('.rx-sticky-post-tools')) {
return;
}
const wrapper = this.createEl('div', 'rx-sticky-post-tools');
wrapper.setAttribute('aria-label', 'Post tools');
const copy = this.createEl('button', 'rx-sticky-tool', 'Copy Link');
copy.type = 'button';
copy.setAttribute('data-rx-copy-link', 'true');
const share = this.createEl('button', 'rx-sticky-tool', 'Share');
share.type = 'button';
share.setAttribute('data-rx-share', 'true');
const print = this.createEl('button', 'rx-sticky-tool', 'Print');
print.type = 'button';
print.addEventListener('click', () => window.print());
wrapper.appendChild(copy);
wrapper.appendChild(share);
wrapper.appendChild(print);
document.body.appendChild(wrapper);
},
insertIntoPostTools(element) {
let tools = document.querySelector('.rx-post-tools');
if (!tools) {
tools = this.createEl('div', 'rx-post-tools');
tools.setAttribute('aria-label', 'Article tools');
const title = document.querySelector('.entry-title, .post-title, h1');
if (title && title.parentNode) {
title.parentNode.insertBefore(tools, title.nextSibling);
} else {
this.state.content.insertBefore(tools, this.state.content.firstChild);
}
}
tools.appendChild(element);
},
createSelectionShare() {
if (this.state.selectionPopup) {
return;
}
const popup = this.createEl('div', 'rx-selection-popup');
popup.setAttribute('aria-hidden', 'true');
const copyButton = this.createEl('button', 'rx-selection-copy', 'Copy');
copyButton.type = 'button';
const shareButton = this.createEl('button', 'rx-selection-share', 'Share');
shareButton.type = 'button';
popup.appendChild(copyButton);
popup.appendChild(shareButton);
document.body.appendChild(popup);
this.state.selectionPopup = popup;
document.addEventListener('mouseup', () => {
window.setTimeout(() => this.updateSelectionPopup(), 20);
});
document.addEventListener('keyup', () => {
this.updateSelectionPopup();
});
document.addEventListener('click', (event) => {
if (!popup.contains(event.target)) {
popup.classList.remove('is-visible');
popup.setAttribute('aria-hidden', 'true');
}
});
copyButton.addEventListener('click', () => {
const selected = window.getSelection().toString().trim();
if (selected) {
this.copyText(`${selected}\n\nSource: ${window.location.href}`);
this.showToast('Selected text copied');
}
});
shareButton.addEventListener('click', () => {
const selected = window.getSelection().toString().trim();
if (!selected) {
return;
}
if (navigator.share) {
navigator.share({
title: document.title,
text: selected,
url: window.location.href,
}).catch(() => {});
} else {
this.copyText(`${selected}\n\nSource: ${window.location.href}`);
this.showToast('Selected text copied');
}
});
},
updateSelectionPopup() {
const popup = this.state.selectionPopup;
const selection = window.getSelection();
if (!popup || !selection || selection.rangeCount === 0) {
return;
}
const selectedText = selection.toString().trim();
if (!selectedText || selectedText.length < 8) {
popup.classList.remove('is-visible');
popup.setAttribute('aria-hidden', 'true');
return;
}
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
if (!rect.width && !rect.height) {
return;
}
popup.style.left = `${window.scrollX + rect.left + rect.width / 2}px`;
popup.style.top = `${window.scrollY + rect.top - 45}px`;
popup.classList.add('is-visible');
popup.setAttribute('aria-hidden', 'false');
},
improveCommentForm() {
const form = document.querySelector('#commentform');
if (!form) {
return;
}
const textarea = form.querySelector('textarea');
if (textarea) {
const counter = this.createEl('div', 'rx-comment-counter', '0 characters');
textarea.parentNode.appendChild(counter);
textarea.addEventListener('input', () => {
counter.textContent = `${textarea.value.length} characters`;
});
}
form.addEventListener('submit', (event) => {
const comment = form.querySelector('textarea');
if (comment && comment.value.trim().length < 3) {
event.preventDefault();
this.showToast('Please write a longer comment');
comment.focus();
}
});
},
restoreReadingPosition() {
const key = `${this.config.storagePrefix}position_${window.location.pathname}`;
const saved = sessionStorage.getItem(key);
if (!saved || window.location.hash) {
return;
}
const position = parseInt(saved, 10);
if (position > 300) {
const restore = window.confirm('Do you want to continue reading from where you left?');
if (restore) {
window.scrollTo({
top: position,
behavior: 'smooth',
});
}
}
},
saveReadingPosition() {
const key = `${this.config.storagePrefix}position_${window.location.pathname}`;
window.addEventListener(
'beforeunload',
() => {
sessionStorage.setItem(key, String(window.scrollY));
},
{ passive: true }
);
},
addKeyboardShortcuts() {
document.addEventListener('keydown', (event) => {
const tag = document.activeElement?.tagName?.toLowerCase();
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
return;
}
if (event.key === 'Escape') {
this.closeLightbox();
}
if (event.key.toLowerCase() === 't') {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
}
if (event.key.toLowerCase() === 'p' && event.ctrlKey) {
return;
}
});
},
copyText(text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text).catch(() => {
this.fallbackCopyText(text);
});
}
this.fallbackCopyText(text);
return Promise.resolve();
},
fallbackCopyText(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed';
textarea.style.top = '-9999px';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
} catch (error) {
console.warn('Copy failed', error);
}
document.body.removeChild(textarea);
},
showToast(message) {
let toast = document.querySelector('.rx-toast');
if (!toast) {
toast = this.createEl('div', 'rx-toast');
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
document.body.appendChild(toast);
}
toast.textContent = message;
toast.classList.add('is-visible');
window.clearTimeout(toast._rxTimeout);
toast._rxTimeout = window.setTimeout(() => {
toast.classList.remove('is-visible');
}, 2200);
},
onScroll() {
if (this.state.ticking) {
return;
}
this.state.ticking = true;
window.requestAnimationFrame(() => {
this.updateReadingProgress();
this.updateBackToTopButton();
this.state.ticking = false;
});
},
onResize() {
this.updateReadingProgress();
},
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => RXSinglePost.init());
} else {
RXSinglePost.init();
}
})();
Add this CSS also, otherwise many features will work but not look beautiful.
File:
rx-theme/inc/css/single-post.css
/* RX Theme Single Post UI */
.rx-reading-progress-bar {
position: fixed;
top: 0;
left: 0;
z-index: 99999;
width: 100%;
height: 4px;
background: currentColor;
transform: scaleX(0);
transform-origin: left center;
pointer-events: none;
}
.rx-reading-time {
margin: 10px 0 20px;
font-size: 14px;
opacity: 0.75;
}
.rx-table-of-contents {
margin: 24px 0;
padding: 20px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 12px;
background: rgba(0, 0, 0, 0.03);
}
.rx-toc-title {
width: 100%;
border: 0;
background: transparent;
font-weight: 700;
font-size: 18px;
text-align: left;
cursor: pointer;
}
.rx-toc-list {
margin: 16px 0 0;
padding-left: 22px;
}
.rx-table-of-contents.is-collapsed .rx-toc-list {
display: none;
}
.rx-toc-item {
margin: 8px 0;
}
.rx-toc-level-3 {
margin-left: 18px;
font-size: 95%;
}
.rx-toc-link {
text-decoration: none;
}
.rx-toc-link.is-active {
font-weight: 700;
text-decoration: underline;
}
.rx-heading-anchor {
margin-left: 8px;
font-size: 70%;
opacity: 0;
text-decoration: none;
}
h2:hover .rx-heading-anchor,
h3:hover .rx-heading-anchor,
h4:hover .rx-heading-anchor,
h5:hover .rx-heading-anchor,
h6:hover .rx-heading-anchor {
opacity: 0.65;
}
.rx-back-to-top {
position: fixed;
right: 20px;
bottom: 24px;
z-index: 9999;
width: 44px;
height: 44px;
border: 0;
border-radius: 999px;
opacity: 0;
visibility: hidden;
cursor: pointer;
transform: translateY(12px);
transition: 0.25s ease;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
}
.rx-back-to-top.is-visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.rx-post-tools {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 16px 0;
}
.rx-post-tools button,
.rx-font-size-button,
.rx-print-button,
.rx-reading-mode-button,
.rx-sticky-tool,
.rx-copy-code-button,
.rx-selection-popup button {
border: 1px solid rgba(0, 0, 0, 0.14);
border-radius: 999px;
padding: 8px 14px;
background: #fff;
cursor: pointer;
font-size: 14px;
}
.rx-sticky-post-tools {
position: fixed;
left: 18px;
top: 45%;
z-index: 9998;
display: flex;
flex-direction: column;
gap: 8px;
}
.rx-sticky-tool {
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.14);
}
.rx-code-ready {
position: relative;
}
.rx-copy-code-button {
position: absolute;
top: 8px;
right: 8px;
}
.rx-external-link::after {
content: "↗";
margin-left: 4px;
font-size: 80%;
}
.rx-clickable-image {
cursor: zoom-in;
}
.rx-lightbox-overlay {
position: fixed;
inset: 0;
z-index: 100000;
display: none;
align-items: center;
justify-content: center;
padding: 24px;
background: rgba(0, 0, 0, 0.86);
}
.rx-lightbox-overlay.is-open {
display: flex;
}
.rx-lightbox-image {
max-width: 92vw;
max-height: 82vh;
object-fit: contain;
}
.rx-lightbox-close {
position: fixed;
top: 18px;
right: 22px;
border: 0;
background: transparent;
color: #fff;
font-size: 42px;
cursor: pointer;
}
.rx-lightbox-caption {
position: fixed;
left: 24px;
right: 24px;
bottom: 18px;
color: #fff;
text-align: center;
}
.rx-lightbox-open {
overflow: hidden;
}
.rx-video-wrapper {
position: relative;
width: 100%;
padding-bottom: 56.25%;
margin: 24px 0;
overflow: hidden;
border-radius: 12px;
}
.rx-video-wrapper iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.rx-table-wrapper {
width: 100%;
overflow-x: auto;
margin: 24px 0;
}
.rx-table-wrapper table {
min-width: 680px;
}
.rx-toast {
position: fixed;
left: 50%;
bottom: 28px;
z-index: 100001;
padding: 10px 16px;
border-radius: 999px;
background: #111;
color: #fff;
opacity: 0;
visibility: hidden;
transform: translate(-50%, 14px);
transition: 0.25s ease;
}
.rx-toast.is-visible {
opacity: 1;
visibility: visible;
transform: translate(-50%, 0);
}
.rx-selection-popup {
position: absolute;
z-index: 100002;
display: none;
gap: 6px;
padding: 6px;
border-radius: 999px;
background: #111;
transform: translateX(-50%);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.25);
}
.rx-selection-popup.is-visible {
display: flex;
}
.rx-selection-popup button {
border-color: transparent;
background: #fff;
}
.rx-comment-counter {
margin-top: 6px;
font-size: 13px;
opacity: 0.7;
}
.rx-dark-reading-mode {
background: #111827;
color: #f9fafb;
}
.rx-dark-reading-mode article,
.rx-dark-reading-mode .entry-content,
.rx-dark-reading-mode .post-content {
color: #f9fafb;
}
.rx-dark-reading-mode a {
color: #93c5fd;
}
.rx-dark-reading-mode .rx-table-of-contents {
border-color: rgba(255, 255, 255, 0.16);
background: rgba(255, 255, 255, 0.06);
}
.rx-dark-reading-mode .rx-post-tools button,
.rx-dark-reading-mode .rx-font-size-button,
.rx-dark-reading-mode .rx-print-button,
.rx-dark-reading-mode .rx-reading-mode-button,
.rx-dark-reading-mode .rx-sticky-tool {
background: #1f2937;
color: #f9fafb;
border-color: rgba(255, 255, 255, 0.18);
}
@media (max-width: 768px) {
.rx-sticky-post-tools {
left: 10px;
right: 10px;
top: auto;
bottom: 78px;
flex-direction: row;
justify-content: center;
}
.rx-sticky-tool {
padding: 7px 10px;
font-size: 12px;
}
.rx-back-to-top {
right: 14px;
bottom: 18px;
}
.rx-table-of-contents {
padding: 16px;
}
}
@media print {
.rx-reading-progress-bar,
.rx-back-to-top,
.rx-post-tools,
.rx-sticky-post-tools,
.rx-copy-code-button,
.rx-selection-popup,
.rx-toast,
.rx-lightbox-overlay {
display: none !important;
}
}
Now enqueue both files from your functions.php or your enqueue file.
function rx_theme_single_post_assets() {
if ( is_single() ) {
wp_enqueue_style(
'rx-single-post',
get_template_directory_uri() . '/inc/css/single-post.css',
array(),
'1.0.0'
);
wp_enqueue_script(
'rx-single-post',
get_template_directory_uri() . '/inc/js/single-post.js',
array(),
'1.0.0',
true
);
}
}
add_action( 'wp_enqueue_scripts', 'rx_theme_single_post_assets' );
This file will give your single post pages a very advanced reader experience: progress bar, table of contents, reading time, image popup, copy buttons, share tools, reading mode, font size controls, video responsiveness, table scroll support, and more.