Skip to main content

Building a Before/After Image Slider in Oracle APEX

I built a Before / After Image Slider in Oracle APEX and here is how it actually went

Oracle APEX JavaScript UI Component
Why I even tried this

I had a page in my APEX app where I wanted to show a before and after image, the kind you see on design and photo editing sites where you drag a handle in the middle and the picture changes. APEX does not give you this out of the box, so I had to build it myself using a plain static content region. This post is basically my notes while I was building it, including the parts that failed.

The first problem I ran into

My first idea was simple. Put two images on top of each other, absolute position both of them, and just change the width of the top one using a range input. Sounds easy right. I tried this and the slider itself worked, but the image quality looked broken while dragging. The "before" image was getting squashed and stretched because when I reduced the width of its wrapper div, the image inside it was also shrinking along with the div instead of staying the same size and just getting clipped.

One issue I faced: object-fit: cover on the inner image was not enough, because the wrapper itself was shrinking. So the browser was resizing the image based on the smaller wrapper width, not clipping it. That gave that weird squished look every time I dragged the handle.

What I tried next

I tried using clip-path instead of resizing the wrapper width. This actually fixed the squashing issue since the image kept its full size and only the visible portion changed. But then a new issue came up on my Chennai office wifi test, the clip-path version was not smooth on mobile Safari, dragging felt laggy compared to Chrome. I did not want to spend more days debugging vendor specific clip-path quirks, so I went back to the wrapper width approach and fixed it a different way.

Finally this approach worked. I kept the width based clipping wrapper, but I fixed the inner image width using JavaScript instead of CSS percentage. So instead of letting the "before" image shrink with its wrapper, I set its width equal to the full slider width in pixels using a CSS variable that gets updated on load and on resize. That way the wrapper clips the image but the image itself never actually changes size, so no squashing.

rough idea of how the handle moves, actual slider has your real images

Getting it to work inside an APEX region

Since APEX static content regions accept raw HTML, CSS and JS in one box, I did not need any plugin for this. I just referenced my images using #APP_FILES# so they load from the application static files, and kept everything scoped under one wrapper class with a data attribute, so if I need two of these sliders on the same page later, they will not clash with each other.

One more small issue I faced, the handle position was resetting to 50 percent every time the page refreshed the region using dynamic actions, since the JS runs again on page load. I left it that way on purpose actually, since a fresh 50 percent split looks cleaner for first time visitors compared to remembering old drag positions.

Full working code

Below is the complete static content region code I am using right now. Paste this directly into a Static Content region source in APEX, no plugin needed. Just replace the two image file names with your own uploaded static application files.

before-after-slider.html
<div class="ba-wrap" data-ba-id="ba1">
  <div class="ba-slider">
    <img class="ba-img ba-after" src="#APP_FILES#after.png" alt="After">
    <div class="ba-before-wrap">
      <img class="ba-img ba-before" src="#APP_FILES#before.jpg" alt="Before">
    </div>
    <span class="ba-tag ba-tag-before">BEFORE</span>
    <span class="ba-tag ba-tag-after">AFTER</span>
    <div class="ba-handle">
      <div class="ba-handle-line"></div>
      <div class="ba-handle-btn">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
          <path d="M14 6l-6 6 6 6" stroke="#2563eb" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
          <path d="M10 6l6 6-6 6" stroke="#2563eb" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
        </svg>
      </div>
    </div>
  </div>
  <p class="ba-hint">Drag the handle to compare &nbsp;&harr;&nbsp;</p>
</div>

<style>
.ba-wrap{
  max-width: 760px;
  margin: 30px auto;
  font-family: 'Segoe UI', Roboto, Arial, sans-serif;
  text-align: center;
}
.ba-slider{
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: 16px;
  background: #f1f5f9;
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12), 0 2px 6px rgba(15,23,42,0.06);
  border: 1px solid #e5e9f0;
  user-select: none;
}
.ba-img{
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
}
.ba-before-wrap{
  position: absolute;
  top: 0; left: 0;
  width: 50%;
  height: 100%;
  overflow: hidden;
  border-right: 2px solid #ffffff;
}
.ba-before-wrap .ba-img{
  width: var(--ba-img-w, 100%);
  max-width: none;
}
.ba-tag{
  position: absolute;
  top: 14px;
  padding: 5px 12px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .06em;
  border-radius: 999px;
  background: rgba(255,255,255,0.85);
  color: #1e293b;
  backdrop-filter: blur(4px);
  box-shadow: 0 2px 6px rgba(0,0,0,0.08);
  animation: baFadeIn .6s ease both;
}
.ba-tag-before{ left: 14px; animation-delay: .1s;}
.ba-tag-after{ right: 14px; animation-delay: .25s;}
.ba-handle{
  position: absolute;
  top: 0;
  left: 50%;
  height: 100%;
  width: 0;
  transform: translateX(-50%);
  cursor: ew-resize;
  z-index: 5;
}
.ba-handle-line{
  position: absolute;
  top: 0; bottom: 0; left: 50%;
  width: 2px;
  transform: translateX(-50%);
  background: #ffffff;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.05);
}
.ba-handle-btn{
  position: absolute;
  top: 50%; left: 50%;
  width: 44px; height: 44px;
  transform: translate(-50%, -50%);
  background: #ffffff;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  gap: 2px;
  box-shadow: 0 4px 14px rgba(15,23,42,0.25);
  transition: transform .15s ease, box-shadow .15s ease;
  animation: baPulse 2.2s ease-in-out 3;
}
.ba-handle:hover .ba-handle-btn,
.ba-handle:active .ba-handle-btn{
  transform: translate(-50%, -50%) scale(1.08);
  box-shadow: 0 6px 18px rgba(37,99,235,0.35);
}
.ba-hint{
  margin-top: 10px;
  font-size: 13px;
  color: #64748b;
  letter-spacing: .02em;
}
@keyframes baFadeIn{
  from{ opacity: 0; transform: translateY(-6px); }
  to{ opacity: 1; transform: translateY(0); }
}
@keyframes baPulse{
  0%, 100% { box-shadow: 0 4px 14px rgba(15,23,42,0.25), 0 0 0 0 rgba(37,99,235,0.35); }
  50%      { box-shadow: 0 4px 14px rgba(15,23,42,0.25), 0 0 0 10px rgba(37,99,235,0); }
}
@media (max-width: 480px){
  .ba-handle-btn{ width: 38px; height: 38px; }
  .ba-tag{ font-size: 10px; padding: 4px 9px; }
}
</style>

<script>
(function(){
  document.querySelectorAll('.ba-wrap').forEach(function(wrap){
    var slider   = wrap.querySelector('.ba-slider');
    var beforeW  = wrap.querySelector('.ba-before-wrap');
    var handle   = wrap.querySelector('.ba-handle');
    var afterImg = wrap.querySelector('.ba-after');
    var dragging = false;

    function syncImageWidth(){
      var w = slider.getBoundingClientRect().width;
      beforeW.style.setProperty('--ba-img-w', w + 'px');
    }

    function setPosition(clientX){
      var rect = slider.getBoundingClientRect();
      var x = clientX - rect.left;
      var pct = Math.min(100, Math.max(0, (x / rect.width) * 100));
      beforeW.style.width = pct + '%';
      handle.style.left = pct + '%';
    }

    function start(e){
      dragging = true;
      slider.style.cursor = 'ew-resize';
      move(e);
    }
    function end(){ dragging = false; }
    function move(e){
      if(!dragging && e.type !== 'mousedown' && e.type !== 'touchstart') return;
      var clientX = e.touches ? e.touches[0].clientX : e.clientX;
      setPosition(clientX);
    }

    handle.addEventListener('mousedown', start);
    slider.addEventListener('mousedown', start);
    window.addEventListener('mousemove', function(e){ if(dragging) move(e); });
    window.addEventListener('mouseup', end);

    handle.addEventListener('touchstart', start, {passive:true});
    slider.addEventListener('touchstart', start, {passive:true});
    window.addEventListener('touchmove', function(e){ if(dragging) move(e); }, {passive:true});
    window.addEventListener('touchend', end);

    window.addEventListener('resize', syncImageWidth);
    afterImg.addEventListener('load', syncImageWidth);
    syncImageWidth();
  });
})();
</script>

Things to keep in mind if you copy this

  • Upload your two images as Application static files first, then swap the file names in the code, keep the same aspect ratio for both images or the slider looks odd while dragging.
  • If you are placing more than one slider on the same page, just change data-ba-id and the class name stays the same, the script already loops through every .ba-wrap on the page so it handles multiple sliders on its own.
  • Test on an actual phone, not just the browser dev tools mobile view, touch dragging behaves a bit different on real devices.
Honestly this took me longer than I expected for something that looks so simple on the frontend. Most of the time went into the image squashing bug, not the drag logic itself. If your before after images are not the same exact dimensions, resize or crop them first, that alone fixes a lot of visual glitches before you even touch the code.

Try it yourself

I have hosted a working version of this on my APEX workspace and also pushed the full code to GitHub in case you want to fork it or read through the commit history of how I changed the approach a few times.

Comments

Popular posts from this blog

Face Detection in Oracle Apex

Face Detection Blog - Embedded JS Jefith Shalin Oracle APEX Developer · May 2025 Live Demo GitHub Oracle APEX · AI Integration A Developer's Real Story Building a Face Detection Attendance System in Oracle APEX A real story of trial, error, and finally making face-api.js actually work inside an APEX app. No fluff — just what happened and what finally worked. Try Live Demo View on GitHub 0.22.2 face-api.js Version 2 APEX Pages Built 4 AJAX Processes face-api.js Oracle APEX JavaScript AI / ML Face Recognition CDN Attendance System PL/SQL The Beginning So here is how it all started I was tasked with building an attendance system for our off...

Screen Recorder in Oracle APEX (Single Page)

Oracle APEX Tutorial Screen Recorder in Oracle APEX : Single Page Build a fully functional browser-based screen recorder inside Oracle APEX using just one Static Content region and native JavaScript. No plugins, no external libraries, no server uploads required. ✍️ Why I Built This I was working on a client project where the support team needed to record screen issues and share them directly from the APEX application, without switching to any external tool. Installing third-party software was not an option on their machines, and every screen recorder extension required IT approval. That is when I thought: the browser already has everything we need. Why not build it right inside APEX? That idea turned into this. &#127916; Start / Stop Recording &#128065; Instant Preview ⬇️ One-click Download &#128266; Audio + Video ...

Sticky Notes Widget Inside Oracle APEX

Oracle APEX Project Building a Sticky Notes Widget in Oracle APEX How I built a fully draggable, color-coded, per-user sticky notes board using jQuery UI, APEX Ajax callbacks, and a bit of patience. Live Demo GitHub Repo Oracle APEX jQuery UI PL/SQL Ajax Callbacks JavaScript CSS Introduction Why I Built This I have been building internal tools on Oracle APEX for a while now, and one thing I always felt was missing was a place where users could quickly jot down thoughts without leaving the page. Think of it like a personal scratchpad that lives right inside the app. I had seen sticky note UIs in some Google products and I thought, how hard can this be in APEX? It turned out to be more interesting than I expected. There were a few wrong tu...