Source Code

<div class="demo">
  <div class="compare" id="compare">
    <div class="side side-a">
      <span class="side-label">Basic</span>
      <ul class="feat-list">
        <li>5 projects</li>
        <li>2 team seats</li>
        <li>Community support</li>
        <li>1 GB storage</li>
        <li>Basic analytics</li>
        <li class="muted">✕ Priority support</li>
        <li class="muted">✕ Custom domains</li>
        <li class="muted">✕ Audit logs</li>
      </ul>
    </div>
    <div class="side side-b">
      <span class="side-label">Pro</span>
      <ul class="feat-list">
        <li>Unlimited projects</li>
        <li>Unlimited seats</li>
        <li>Priority support</li>
        <li>100 GB storage</li>
        <li>Advanced analytics</li>
        <li>✓ Priority support</li>
        <li>✓ Custom domains</li>
        <li>✓ Audit logs</li>
      </ul>
    </div>
    <div class="handle" id="handle" role="slider" tabindex="0" aria-label="Compare Basic and Pro plans" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50">
      <div class="handle-line"></div>
      <div class="handle-grip">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M8 6 3 12l5 6M16 6l5 6-5 6"/></svg>
      </div>
    </div>
    <div class="tag tag-a">BASIC</div>
    <div class="tag tag-b">PRO</div>
  </div>
  <p class="hint">Drag the handle, or use ← → keys when focused</p>
</div>

Plan Comparison Slider — Drag to Reveal Basic vs Pro Plan Features

Plan Comparison Slider — Drag to Reveal Basic vs Pro Features · Pricing · Plain HTML, CSS & JS · Live preview

What's included

Features

Two stacked panels revealed via a live clip-path inset, not a translateX trick, so text never visually shifts
Pointer Events API with setPointerCapture — one code path for mouse, touch and pen input
Draggable handle plus full ArrowLeft/ArrowRight keyboard support
Real role="slider" element with aria-valuemin/max/now kept in sync on every interaction
Clamped range (4%–96%) so the handle never fully hides either plan's label
Light Basic panel and dark Pro panel styled independently for clear visual contrast
Static BASIC/PRO corner tags stay legible regardless of handle position
Zero dependencies — pure CSS clip-path plus vanilla JS pointer handling

About this UI Snippet

Drag-to-Compare Plan Slider — A Split-Panel Feature Comparison

Screenshot of the Plan Comparison Slider — Drag to Reveal Basic vs Pro Features snippet rendered live

This component borrows the mechanics of a before/after image slider and applies them to a much more common pricing-page need: showing a visitor exactly what changes when they move from a cheaper plan to a more expensive one. Instead of comparing two photos, dragging the handle compares two feature lists stacked in the same physical space.

How the two-layer clip-path works

The trick is two absolutely positioned .side panels occupying the exact same box — .side-a (Basic) sits underneath in normal light styling, and .side-b (Pro, dark styling) sits on top with clip-path: inset(0 0 0 50%). That clip-path hides everything in .side-b to the left of the 50% mark, letting the Basic panel underneath show through. Dragging the handle simply updates that left-inset percentage in JavaScript — inset(0 0 0 ${pct}%) — which is why the Pro panel appears to "wipe in" from the right as you drag left, without ever animating the feature list items themselves.

Why pointer events instead of separate mouse/touch handlers

The drag logic uses pointerdown/pointermove/pointerup with setPointerCapture, which unifies mouse, touch, and pen input into one event model and keeps receiving move events even if the cursor leaves the handle's bounding box mid-drag — a common failure mode with plain mousemove listeners bound only to the handle itself.

Keyboard accessibility built in from the start

The handle is a real <div role="slider" tabindex="0"> with aria-valuemin, aria-valuemax, and a live aria-valuenow that updates on every drag or arrow-key press — so a screen reader announces the current comparison position as a percentage, and keyboard users can operate the entire comparison with the ArrowLeft/ArrowRight keys alone, with no mouse required at all.

Where this earns its place over a static comparison table

Traditional side-by-side pricing tables ask a visitor to scan two columns and mentally diff them row by row. This slider instead makes the *act of comparing* physical and continuous — sliding toward "Pro" viscerally shows more of the upgraded feature set filling the frame, which tends to read as more persuasive on a pricing page than a static checkmark grid, while still degrading gracefully to a readable feature list at either extreme.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to walk through exactly why clip-path: inset() is a better fit here than translateX or width-based reveals, and to explain how setPointerCapture prevents the drag from breaking if the cursor moves faster than the handle can visually follow. It's also worth asking for a version that supports three or more plans via a horizontal multi-stop slider, or one that snaps to discrete tier positions instead of tracking the pointer continuously.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a draggable two-panel comparison slider in HTML, CSS and vanilla JavaScript that reveals a "Pro" plan's feature list over a "Basic" plan's feature list as the user drags a central handle left and right — no external libraries.

Requirements:
- Two absolutely-positioned panels occupying the same box, each listing plan features; the top panel must be revealed using CSS clip-path: inset(), not width or transform tricks.
- A draggable circular handle positioned at the clip boundary, operable via the Pointer Events API (pointerdown/pointermove/pointerup with setPointerCapture) so it works for mouse, touch and pen input uniformly.
- The handle must be a real role="slider" element with aria-valuemin, aria-valuemax, and a live aria-valuenow attribute that updates on every drag.
- Full keyboard support: when the handle has focus, ArrowLeft and ArrowRight keys must move the comparison position in fixed increments.
- Clamp the draggable range so neither panel can be fully hidden — leave at least a few percent of each side always visible.
- Visually distinguish the two panels (e.g. light vs dark background) and label each with a small corner tag.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Edit the two feature listsUpdate the <li> items inside .side-a and .side-b in the HTML panel to match your own plan tiers.
  2. 2
    Keep list item counts alignedFor a clean comparison, keep the same number of <li> rows in both lists so items line up vertically as the handle moves.
  3. 3
    Adjust the starting positionChange the 50 passed to setPos(50) at the bottom of the JS panel to start the slider closer to one plan.
  4. 4
    Restyle the dark Pro panelUpdate the background, text and accent colors on .side-b in the CSS panel to match your brand instead of the default dark theme.
  5. 5
    Test keyboard operationClick the handle to focus it, then press the left and right arrow keys to confirm the slider is fully operable without a mouse.

Real-world uses

Common Use Cases

SAAS
SaaS Pricing Pages
Let a visitor physically drag between your cheapest and most popular tier to see exactly what upgrading unlocks.
SALES
Sales Enablement Decks
Embed in an interactive one-pager so a rep can demo the value gap between tiers live on a call.
ONBOARD
In-App Upgrade Prompts
Show a free-tier user a compelling side-by-side of what a paid upgrade adds, right where they hit a limit.
AB
A/B Testing Pricing Framing
Compare against a static comparison table to see whether an interactive framing improves upgrade conversion.
Related: Pricing Add-On Selector
See the Pricing Add-On Selector for a related pricing pattern worth pairing with this one.
Related: Pricing Model Toggle (Per-Seat vs Per-Usage)
See the Pricing Model Toggle (Per-Seat vs Per-Usage) for a related pricing pattern worth pairing with this one.
Related: Pricing Table with Sticky Compare Bar
See the Pricing Table with Sticky Compare Bar for a related pricing pattern worth pairing with this one.
Related: Pricing ROI and Breakeven Calculator
See the Pricing ROI and Breakeven Calculator for a related pricing pattern worth pairing with this one.
Related: Role-Based Seat Mix Pricing Calculator
See the Role-Based Seat Mix Pricing Calculator for a related pricing pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Yes — it uses the Pointer Events API (pointerdown/pointermove/pointerup) which normalizes mouse, touch and pen input into a single set of handlers, so dragging works identically on a phone screen.

The clip-path technique is inherently two-panel. For three or more tiers, use a traditional side-by-side comparison table or a tabbed switcher instead — this component is best suited to a direct two-plan comparison.

clip-path: inset() clips the top panel precisely without affecting layout flow or triggering reflow on the panel underneath, which keeps the drag interaction smooth even at high frame rates.

Yes — the handle is a role="slider" element with aria-valuemin, aria-valuemax and a live aria-valuenow, and it supports ArrowLeft/ArrowRight keyboard operation in addition to drag.

The setPos function already clamps the percentage between 4 and 96 so a sliver of both panels always remains visible — adjust those two numbers to change the clamp range.

Yes — add a CSS transition on clip-path to .side-b for eased movement, though for a live drag interaction most implementations keep it instant (transition: 0.05s) so it feels directly connected to the pointer.