User experience (UX) is the invisible backbone of successful websites. When visitors land on your blog, they ask one silent question: “How much time will this take?”
If an article looks daunting, they bounce.
Two simple features drastically reduce bounce rates and improve content engagement:
- Estimated Reading Time: Gives readers a clear expectation before they start.
- Visual Progress Bar: Provides real-time feedback as they scroll, encouraging them to finish.
While dozens of plugins offer this functionality, many come bloated with tracking scripts, heavy database configurations, and unnecessary HTTP requests that slow down your site.
In this comprehensive guide, we will build a complete, production-ready WordPress plugin called Simple Read Progress from scratch. We will use pure PHP, CSS, and vanilla JavaScript—no external libraries, no frameworks, and zero database bloat.
Why Choose a Custom Plugin Over Existing Solutions?
Before diving into the code, let’s address the elephant in the room: why write code when you can install a plugin from the repository?
- Performance Optimization: Commercial plugins often load massive asset files on every single page of your site, including your homepage and contact page. Our custom plugin will selectively load its minimal footprint only on single blog posts.
- Security Control: By writing the code yourself, you know exactly what executes on your server. No hidden tracking scripts, no third-party API calls.
- Design Control: Instead of fighting with a plugin’s complex settings dashboard, you control the layout, colors, and behavior directly through a single, easily editable file.
Architecture of the Plugin
Our plugin will be housed in a single file named read-progress.php. It hooks into the WordPress ecosystem using three core mechanisms:
- Plugin Header: Registers the file as a valid WordPress plugin.
- The Content Filter (
the_content): Intercepts the post body, calculates the word count, appends the “Estimated Reading Time” block, and injects the HTML wrapper for the progress bar. - Asset Enqueuing (
wp_enqueue_scripts): Inline injects our minimal CSS and JavaScript directly into the page header and footer to eliminate unnecessary HTTP requests.
Step-by-Step Code Breakdown
Let’s dissect the core components of our plugin to understand how the data flows safely from the WordPress database to the user’s browser.
1. Security First: Preventing Direct Access
Every professional WordPress file must start with a security check. If a malicious actor tries to access our plugin file directly via https://yourdomain.com, we must kill the execution immediately.
php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
Use code with caution.
2. Math Behind the Reading Time
The average adult reads at a speed of roughly 200 words per minute (WPM). To calculate the reading time, we use standard PHP string utilities:
strip_tags(): Removes HTML markup so we only count viewable text.str_word_count(): Accurately counts individual words.ceil(): Rounds up fractions. If an article takes 1.2 minutes to read, it safely displays as a “2 min” read.
3. Sanitization and Escaping
WordPress standards strictly demand that all dynamic data outputted to the browser must be escaped. We use esc_html__() for internationalization-ready plain text to prevent Cross-Site Scripting (XSS) vulnerabilities.
The Complete Plugin Source Code
Create a new file named read-progress.php inside your /wp-content/plugins/ directory (or create a subfolder named simple-read-progress and place it there). Copy and paste the following complete code block:
php
<?php
/**
* Plugin Name: Simple Read Progress
* Plugin URI: https://example.com
* Description: Adds an estimated reading time above post content and a sticky scrolling progress bar at the top of the browser.
* Version: 1.0.0
* Author: Senior WordPress Developer
* Author URI: https://example.com
* License: GPLv2 or later
* Text Domain: simple-read-progress
*/
// Prevent direct file access to maximize security.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Core class to handle reading time logic and UI asset injection.
*/
class Simple_Read_Progress {
/**
* Constructor sets up WordPress hooks.
*/
public function __construct() {
// Only run on the front end.
if ( ! is_admin() ) {
add_filter( 'the_content', array( $this, 'render_reading_elements' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'inject_assets' ) );
}
}
/**
* Calculates reading time and prepends elements to single post content.
*
* @param string $content The original post content.
* @return string Modified content containing the reading time and progress bar container.
*/
public function render_reading_elements( $content ) {
// Apply checks: target only single blog posts within the main query loop.
if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
return $content;
}
// 1. Calculate Estimated Reading Time
$raw_content = strip_tags( $content );
$word_count = str_word_count( $raw_content );
// Define average reading speed (Words Per Minute)
$wpm = 200;
$reading_time = ceil( $word_count / $wpm );
// Build the localized reading time HTML string safely.
$reading_time_html = sprintf(
'<div class="srp-reading-time">%s %d %s</div>',
esc_html__( 'Estimated Reading Time:', 'simple-read-progress' ),
absint( $reading_time ),
esc_html__( 'mins', 'simple-read-progress' )
);
// 2. Build the Progress Bar HTML markup container
$progress_bar_html = '<div id="srp-progress-container" role="presentation"><div id="srp-progress-bar"></div></div>';
// Prepend both elements neatly above the main content payload
return $progress_bar_html . $reading_time_html . $content;
}
/**
* Injects lightweight structural CSS styles and high-performance vanilla JavaScript.
*/
public function inject_assets() {
// Enqueue assets strictly on single posts to protect site-wide speed performance.
if ( ! is_singular( 'post' ) ) {
return;
}
// Inline CSS: Eliminates an external HTTP request asset block
$custom_css = "
#srp-progress-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background-color: transparent;
z-index: 99999;
pointer-events: none;
}
#srp-progress-bar {
height: 100%;
width: 0%;
background-color: #2196F3;
transition: width 0.1s ease-out;
}
.srp-reading-time {
font-size: 0.9rem;
color: #666;
margin-bottom: 1.5rem;
font-style: italic;
display: flex;
align-items: center;
gap: 0.25rem;
}
";
wp_register_style( 'srp-inline-styles', false );
wp_enqueue_style( 'srp-inline-styles' );
wp_add_inline_style( 'srp-inline-styles', $custom_css );
// Inline JavaScript: High performance scroll tracking using requestAnimationFrame
$custom_js = "
document.addEventListener('DOMContentLoaded', function() {
var progressBar = document.getElementById('srp-progress-bar');
if (!progressBar) return;
var ticking = false;
function updateProgress() {
var winScroll = window.scrollY || document.documentElement.scrollTop;
var height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
if (height > 0) {
var scrolled = (winScroll / height) * 100;
progressBar.style.width = Math.min(Math.max(scrolled, 0), 100) + '%';
} else {
progressBar.style.width = '0%';
}
ticking = false;
}
window.addEventListener('scroll', function() {
if (!ticking) {
window.requestAnimationFrame(updateProgress);
ticking = true;
}
}, { passive: true });
// Initial run calculation in case page loads down-scroll cached
updateProgress();
});
";
wp_register_script( 'srp-inline-js', false, array(), null, true );
wp_enqueue_script( 'srp-inline-js' );
wp_add_inline_script( 'srp-inline-js', $custom_js );
}
}
// Initialize the plugin system object.
new Simple_Read_Progress();
Use code with caution.
Detailed Technical Insights
JavaScript Optimization via requestAnimationFrame
Traditional scroll listeners execute code continuously every single pixel the user scrolls. This causes massive layout thrashing and stuttering UI frames, particularly on mobile devices.
To prevent lag, our plugin implements two critical JavaScript optimizations:
- Passive Event Listeners:
{ passive: true }informs the browser that our scroll event will never callpreventDefault(). This allows the browser to scroll the page instantly without waiting for the JS engine thread execution. requestAnimationFrameTicking: We lock execution calls to match the browser’s natural screen refresh cycle (usually 60Hz or 120Hz). Even if a mouse wheel fires 50 scroll inputs in a millisecond, our calculation triggers only when the screen is actually ready to repaint its graphics layout.
Smart Filter Context Checking
Notice the safety conditions inside render_reading_elements:
php
if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
return $content;
}
Use code with caution.
Without checking in_the_loop() and is_main_query(), your progress bar could run multiple times on a single page if you have custom sidebar widgets showing “Recent Posts” or related content loops underneath your article text. This strict conditional logic isolates our feature cleanly to the primary article content panel.
How to Install and Customize Your Plugin
- Connect to your site via SFTP or open your hosting control panel’s File Manager.
- Navigate to
/wp-content/plugins/. - Create a folder named
simple-read-progress. - Create a file inside named
read-progress.phpand paste the full script code block shown above. - Log into your WordPress Dashboard, click Plugins, find Simple Read Progress, and hit Activate.
Fine-Tuning Styles
If you want to alter the bar’s color scheme or width profile to align perfectly with your existing brand aesthetic, locate the inline CSS block within the code.
- To alter the progress bar color, change
#2196F3to any hexadecimal value (e.g.,#ff0000for deep red). - To change thickness, adjust the
height: 4px;rule inside#srp-progress-containerto your preferred pixel dimension.
Final Thoughts
You just created an ultra-lightweight, high-performance plugin that adds modern, conversion-boosting UX elements to your blog. By prioritizing clean standard practices over clunky third-party dependencies, your website retains lightning-fast load times while providing visitors with an engaging reading experience.