Content Marketing

WordPress: How To Build a Shortcode Plugin For A Simple Before and After Image Slider

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:

Before
After

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:

  1. Create a new folder in the wp-content/plugins/ directory and name it before-after-slider.
  2. Create a new file inside that folder and name it before-after-slider.php.
  3. Copy and paste the following code into the before-after-slider.php file.
  4. 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

  1. Security check At the very top, the plugin includes a guard clause:
if ( ! defined( 'ABSPATH' ) ) exit;
  1. This ensures the file can only be executed inside a WordPress environment. If someone tries to access the file directly, it exits immediately.
  2. 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.
  3. 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.
    If either before or after is missing, the shortcode outputs an error message instead of a broken slider. The generated HTML structure looks like this:
<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.

Douglas Karr

Douglas Karr is a fractional Chief Marketing Officer specializing in SaaS and AI companies, where he helps scale marketing operations, drive demand generation, and implement AI-powered strategies. He is the founder and publisher of Martech Zone, a leading publication in marketing technology, and a trusted advisor to startups and enterprises… More »
Back to top button
Close

Adblock Detected

We rely on ads and sponsorships to keep Martech Zone free. Please consider disabling your ad blocker—or support us with an affordable, ad-free annual membership ($10 US):

Sign Up For An Annual Membership