I was publishing an article earlier that included a before-and-after image with a slider overlay. While there are numerous plugins available that perform this function, I prefer not to use third-party plugins due to the additional overhead, including limited features and advertisements for upgrades. I like to keep my WordPress instance as lean as possible, so I created a plugin that utilizes WordPress and jQuery to display the before-and-after images.
Before and After Image Slider
Example:
Here’s the shortcode that produced it:
[before_after before="https://cdn.martech.zone/wp-content/uploads/2025/04/black-white.webp" after="https://cdn.martech.zone/wp-content/uploads/2025/04/full-color.wepb" width="100%" class="app-container"]
How to Install the Before and After Slider Plugin
Follow these steps to install and activate the plugin on your WordPress site:
- Create a new folder in the
wp-content/plugins/
directory and name itbefore-after-slider
. - Create a new file inside that folder and name it
before-after-slider.php
. - Copy and paste the following code into the
before-after-slider.php
file. - Save the file and activate the plugin from the WordPress admin dashboard.
When the page loads:
- If the shortcode is detected, the jQuery script and CSS are injected into the page header.
- The script initializes the slider behavior, setting the handle in the middle (50%) initially.
- Users interact with the handle to reveal more or less of the before or after image dynamically.
<?php
/**
* Plugin Name: Before/After Slider Shortcode
* Description: Adds a responsive, accessible before/after image slider via [before_after] with optional wrapper class, attachment ID or URL support, WebP/AVIF compatibility, and fluid width via width="100%".
* Version: 1.1.0
* Author: Douglas Karr
* Author URl: https://dknewmedia.com
* License: GPL-2.0-or-later
* Text Domain: before-after-slider
*/
if ( ! defined( 'ABSPATH' ) ) { exit; }
if ( ! class_exists( 'DKNM_BeforeAfterSlider' ) ) :
final class DKNM_BeforeAfterSlider {
public static function init() {
add_shortcode( 'before_after', [ __CLASS__, 'shortcode' ] );
add_action( 'wp_enqueue_scripts', [ __CLASS__, 'maybe_enqueue_assets' ] );
}
/**
* Enqueue small inline CSS/JS only on pages that contain the shortcode.
*/
public static function maybe_enqueue_assets() {
if ( is_admin() ) { return; }
global $post;
if ( ! ( is_a( $post, 'WP_Post' ) && has_shortcode( (string) $post->post_content, 'before_after' ) ) ) {
return;
}
// Minimal CSS
$css = '
.ba-wrap { width: 100%; }
.ba-slider { position: relative; width: 100%; overflow: hidden; touch-action: none; user-select: none; }
.ba-track { position: relative; width: 100%; height: 100%; }
.ba-before, .ba-after { position: absolute; inset: 0; }
.ba-before img, .ba-after img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; }
.ba-handle { position: absolute; top: 0; bottom: 0; width: 4px; background: #fff; box-shadow: 0 0 5px rgba(0,0,0,.3); cursor: ew-resize; z-index: 10; }
.ba-handle::before { content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: 20px; height: 20px; background: #fff; border-radius: 50%; box-shadow: 0 0 5px rgba(0,0,0,.3); }
.ba-slider[aria-disabled="true"] .ba-handle { cursor: not-allowed; opacity: .6; }';
wp_register_style( 'dknm-ba-inline-style', false, [], null );
wp_enqueue_style( 'dknm-ba-inline-style' );
wp_add_inline_style( 'dknm-ba-inline-style', $css );
// JS using Pointer Events + rAF + keyboard support
$js = '
(function(){
function init(root){
var before = root.querySelector(".ba-before");
var after = root.querySelector(".ba-after");
var handle = root.querySelector(".ba-handle");
var track = root.querySelector(".ba-track");
if(!before || !after || !handle || !track){ return; }
var pos = 50; // percent
function apply(p){
pos = Math.max(0, Math.min(100, p));
before.style.clipPath = "inset(0 " + (100 - pos) + "% 0 0)";
after.style.clipPath = "inset(0 0 0 " + pos + "%)";
handle.style.left = pos + "%";
root.setAttribute("aria-valuenow", Math.round(pos));
}
function percentFromClientX(x){
var rect = track.getBoundingClientRect();
if (rect.width <= 0) return pos;
return ((x - rect.left) / rect.width) * 100;
}
var dragging = false, rafPending = false, nextPos = pos;
function onPointerMove(e){
if(!dragging) return;
nextPos = percentFromClientX(e.clientX);
if(!rafPending){
rafPending = true;
requestAnimationFrame(function(){ apply(nextPos); rafPending = false; });
}
}
function onPointerDown(e){
e.preventDefault();
dragging = true;
try { track.setPointerCapture(e.pointerId); } catch(_){}
onPointerMove(e);
}
function onPointerUp(){
dragging = false;
}
track.addEventListener("pointerdown", onPointerDown, {passive:false});
window.addEventListener("pointermove", onPointerMove, {passive:false});
window.addEventListener("pointerup", onPointerUp, {passive:true});
window.addEventListener("resize", function(){ apply(pos); });
// Keyboard accessibility
root.addEventListener("keydown", function(e){
var step = (e.shiftKey ? 10 : 2);
if(e.key === "ArrowLeft"){ apply(pos - step); e.preventDefault(); }
else if(e.key === "ArrowRight"){ apply(pos + step); e.preventDefault(); }
else if(e.key === "Home"){ apply(0); e.preventDefault(); }
else if(e.key === "End"){ apply(100); e.preventDefault(); }
});
apply(pos);
}
document.addEventListener("DOMContentLoaded", function(){
document.querySelectorAll(".ba-slider[role=\'slider\']").forEach(init);
});
})();';
wp_register_script( 'dknm-ba-inline-script', '', [], null, true );
wp_enqueue_script( 'dknm-ba-inline-script' );
wp_add_inline_script( 'dknm-ba-inline-script', $js );
}
/**
* Shortcode handler.
* width supports either a number (max-width in px) or exactly "100%" for fluid width.
*
* Examples:
* [before_after before="https://example.com/before.webp" after="https://example.com/after.webp" width="100%" ratio="4/3" class="container my-6"]
* [before_after before_id="123" after_id="456" width="1000" height="500" class="container"]
*/
public static function shortcode( $atts ) {
$atts = shortcode_atts( [
'before' => '',
'after' => '',
'before_id' => '',
'after_id' => '',
'width' => '800', // number = max-width px, or "100%" for fluid width
'height' => '', // fixed height in px; if empty, uses aspect-ratio
'ratio' => '16/9', // used when height is empty
'class' => '', // wrapper classes
'alt_before' => 'Before',
'alt_after' => 'After',
], $atts, 'before_after' );
$wrapper_classes = self::sanitize_classes( $atts['class'] );
// Dimension style: either a fixed height, or aspect-ratio
if ( $atts['height'] !== '' ) {
$style_dim = 'height:' . (int) $atts['height'] . 'px;';
} else {
$style_dim = 'aspect-ratio:' . esc_attr( $atts['ratio'] ) . ';';
}
// Width style: special case for "100%" to enable full fluid width
if ( trim( $atts['width'] ) === '100%' ) {
$width_style = 'width:100%;';
} else {
$max_width = (int) $atts['width'] > 0 ? (int) $atts['width'] : 800;
$width_style = 'max-width:' . $max_width . 'px;';
}
$before_html = self::build_img_html( $atts['before_id'], $atts['before'], $atts['alt_before'], 'ba-img ba-img-before' );
$after_html = self::build_img_html( $atts['after_id'], $atts['after'], $atts['alt_after'], 'ba-img ba-img-after' );
if ( ! $before_html || ! $after_html ) {
return '<p>' . esc_html__( 'Error: Both before and after images are required. Provide URLs via before=/after= or attachment IDs via before_id=/after_id=.', 'before-after-slider' ) . '</p>';
}
$wrap_open = $wrapper_classes ? '<div class="ba-wrap ' . esc_attr( $wrapper_classes ) . '">' : '<div class="ba-wrap">';
$html = $wrap_open;
$html .= '<div class="ba-slider" role="slider" aria-label="' . esc_attr__( 'Before and after slider', 'before-after-slider' ) . '" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50" tabindex="0" style="' . $width_style . $style_dim . '">';
$html .= ' <div class="ba-track">';
$html .= ' <div class="ba-before">' . $before_html . '</div>';
$html .= ' <div class="ba-after">' . $after_html . '</div>';
$html .= ' <div class="ba-handle" style="left:50%"></div>';
$html .= ' </div>';
$html .= '</div>';
$html .= '</div>';
return $html;
}
/**
* Build an <img> tag using attachment ID if available (gets srcset/WebP/AVIF via WordPress),
* otherwise fall back to a direct URL (works fine with .webp).
*/
private static function build_img_html( $maybe_id, $maybe_url, $alt, $classes ) {
$alt = (string) $alt;
if ( is_numeric( $maybe_id ) && (int) $maybe_id > 0 ) {
$img = wp_get_attachment_image( (int) $maybe_id, 'full', false, [
'class' => $classes,
'loading' => 'lazy',
'decoding' => 'async',
'alt' => $alt,
] );
if ( $img ) return $img;
}
if ( ! empty( $maybe_url ) ) {
return sprintf(
'<img class="%s" src="%s" alt="%s" loading="lazy" decoding="async" />',
esc_attr( $classes ),
esc_url( $maybe_url ),
esc_attr( $alt )
);
}
return '';
}
/**
* Sanitize space-separated classes (allows multiple classes).
*/
private static function sanitize_classes( $classes_raw ) {
if ( ! $classes_raw ) return '';
$classes = preg_split( '/\s+/', (string) $classes_raw );
$san = [];
foreach ( $classes as $c ) {
$c = sanitize_html_class( $c );
if ( $c ) { $san[] = $c; }
}
return implode( ' ', array_unique( $san ) );
}
}
DKNM_BeforeAfterSlider::init();
endif;
Here’s a rewritten, clearer usage guide for your plugin that walks through what it does and how to use it, with examples:
How the plugin works
- Security check At the very top, the plugin includes a guard clause:
if ( ! defined( 'ABSPATH' ) ) exit;
- This ensures the file can only be executed inside a WordPress environment. If someone tries to access the file directly, it exits immediately.
- Loading CSS and JavaScript The plugin only loads its assets when needed. It checks whether the current post or page contains the
Error: Both before and after images are required. Provide URLs via before=/after= or attachment IDs via before_id=/after_id=.
shortcode. If not, it skips loading CSS and JavaScript entirely—keeping your site lean. When the shortcode is present, it hooks into WordPress’s wp_enqueue_scripts action to add:- Inline CSS to style the slider:
- Positions the before/after images on top of each other with absolute positioning.
- Uses clip-path so only part of each image is visible at a time.
- Styles the draggable handle as a vertical line with a circular knob for user interaction.
- Inline JavaScript that powers the interactivity:
- Initializes the slider at 50% visibility for both images.
- Listens for mouse, touch, and pointer events.
- Updates the handle’s position and adjusts the visible parts of each image in real time as the user drags or taps.
- Inline CSS to style the slider:
- Shortcode functionality The
Error: Both before and after images are required. Provide URLs via before=/after= or attachment IDs via before_id=/after_id=.
shortcode outputs the HTML for the slider. It accepts several attributes:- before — URL of the “before” image (or use before_id for a WordPress attachment ID).
- after — URL of the “after” image (or use after_id for a WordPress attachment ID).
- width — either a numeric pixel max-width (e.g., 800) or “100%” for full fluid width.
- height — fixed pixel height. If omitted, the slider uses an aspect-ratio (default 16/9).
- class — optional wrapper classes (e.g., container my-slider).
- alt_before and alt_after — optional alt text for accessibility.
<div class="ba-wrap container">
<div class="ba-slider" style="width:100%; aspect-ratio:16/9;" role="slider">
<div class="ba-track">
<div class="ba-before"><img src="before-image.webp" alt="Before"></div>
<div class="ba-after"><img src="after-image.webp" alt="After"></div>
<div class="ba-handle" style="left:50%"></div>
</div>
</div>
</div>
Example usage
Basic example with image URLs:
[before_after before="https://example.com/images/room-before.webp" after="https://example.com/images/room-after.webp" width="800" ratio="4/3"]
This will output a slider, 800px wide with a 4:3 aspect ratio, comparing two .webp images.
Full-width responsive example:
[before_after before="https://example.com/images/before.jpg" after="https://example.com/images/after.jpg" width="100%" ratio="16/9" class="full-bleed"]
This will stretch the slider to fill its container, maintain a 16:9 ratio, and apply the full-bleed class for extra styling.
Using attachment IDs (recommended for WebP/AVIF + responsive srcset):
[before_after before_id="123" after_id="456" width="1000" height="500" class="my-slider"]
This version lets WordPress handle responsive images and modern formats automatically.