Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions submissions/examples/before-after-image-slider-em/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Before / After Comparison Slider

## 1. What does this do?

This is a self-contained example (resolves [#55696](https://github.com/SAPTARSHI-coder/EaseMotion-css/issues/55696)) that adds an interactive, draggable before/after image comparison slider — the kind used in design-portfolio and photo-retouching showcases to reveal a "before" and "after" state side by side.

It includes two working instances:

- **Darkroom grade** — a desaturated landscape scene sliding into a vivid color-graded version, demonstrating the component on illustrative imagery.
- **Interface redesign** — a wireframe-style UI mockup sliding into its redesigned counterpart, demonstrating the component on flat UI content instead of a photo.

Both instances share the same markup pattern and are driven entirely by one CSS custom property, `--clip-offset`.

## 2. How is it used?

Open `demo.html` in a browser — no build step, no dependencies. Drag the circular handle left/right, tap and swipe on mobile, or click the handle and use the **Left/Right arrow keys** to nudge the comparison point.

The core pattern, consistent with the [component conventions](../../../README.md) used elsewhere in this repo:

```html
<div class="compare" style="--clip-offset: 50%">
<div class="compare-frame">
<div class="compare-layer compare-layer--before">...before content...</div>
<div class="compare-layer compare-layer--after">...after content...</div>
<div class="compare-handle"></div>
</div>
<input type="range" class="compare-range" min="0" max="100" value="50" />
</div>
```

```css
.compare-layer--after {
clip-path: inset(0 calc(100% - var(--clip-offset)) 0 0);
}
```

```js
// script.js
range.addEventListener("input", () => {
container.style.setProperty("--clip-offset", `${range.value}%`);
});
```

The "after" layer sits on top of the "before" layer and is clipped from the right edge by `--clip-offset`. A native `<input type="range">` is stretched over the whole frame, fully transparent except for its thumb, which is styled to look like the visible handle grip. Because it's a real range input, dragging, touch swiping, and arrow-key nudging all come from the browser for free — `script.js` only has to copy the input's value onto the CSS variable.

## 3. Why is it useful?

Most before/after sliders reach for `mousemove`/`touchmove` listeners and manual position math, which means re-implementing drag physics, touch handling, and keyboard support separately. Anchoring the whole interaction to a single custom property and a native range input removes almost all of that: the browser already knows how to drag, swipe, and step a range control, and CSS already knows how to clip a layer — this component just wires the two together. That makes it easy to drop a second, third, or fourth instance onto a page (as the two examples here show) without writing any additional interaction code, only new markup for the before/after content itself.
256 changes: 256 additions & 0 deletions submissions/examples/before-after-image-slider-em/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Before / After Comparison Slider — EaseMotion CSS</title>
<meta
name="description"
content="A draggable before/after image comparison slider built with EaseMotion CSS — clip-path driven, keyboard accessible, touch-friendly."
/>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main class="wrap">
<!-- ============ HERO ============ -->
<header class="hero">
<p class="eyebrow"><span class="dot"></span> submissions/examples · before-after-image-slider-em</p>
<h1 class="hero-title">
Same frame.<br />
<span class="hl">Two exposures.</span>
</h1>
<p class="hero-sub">
One slider, one CSS custom property. Drag the grip, use the arrow keys, or touch
and swipe — <code>--clip-offset</code> does the rest, no JavaScript animation
loop required.
</p>
</header>

<!-- ============ SIGNATURE: COMPARISON SLIDER 1 ============ -->
<section class="demo-block" aria-labelledby="demo-1-title">
<div class="demo-head">
<h2 id="demo-1-title" class="demo-title">Darkroom grade</h2>
<span class="demo-tag">landscape · color grade</span>
</div>

<div class="compare" style="--ratio: 16/10">
<div class="compare-frame">
<!-- BEFORE layer -->
<div class="compare-layer compare-layer--before">
<svg viewBox="0 0 640 400" preserveAspectRatio="xMidYMid slice" class="scene scene--before" aria-hidden="true">
<rect width="640" height="400" fill="#3a3d42"/>
<circle cx="500" cy="90" r="46" fill="#6b6e73"/>
<path d="M0 260 L140 150 L230 230 L340 120 L430 220 L520 160 L640 250 L640 400 L0 400 Z" fill="#26282c"/>
<path d="M0 310 L180 240 L300 300 L460 230 L640 300 L640 400 L0 400 Z" fill="#1b1d20"/>
</svg>
<span class="scene-label scene-label--before">Before</span>
</div>

<!-- AFTER layer, clipped by --clip-offset -->
<div class="compare-layer compare-layer--after">
<svg viewBox="0 0 640 400" preserveAspectRatio="xMidYMid slice" class="scene scene--after" aria-hidden="true">
<defs>
<linearGradient id="sky1" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ff9a5c"/>
<stop offset="55%" stop-color="#f3667a"/>
<stop offset="100%" stop-color="#5b3a8f"/>
</linearGradient>
</defs>
<rect width="640" height="400" fill="url(#sky1)"/>
<circle cx="500" cy="90" r="46" fill="#ffe3a8"/>
<path d="M0 260 L140 150 L230 230 L340 120 L430 220 L520 160 L640 250 L640 400 L0 400 Z" fill="#3d2b52" opacity="0.9"/>
<path d="M0 310 L180 240 L300 300 L460 230 L640 300 L640 400 L0 400 Z" fill="#241a38"/>
</svg>
<span class="scene-label scene-label--after">After</span>
</div>

<!-- Handle -->
<div class="compare-handle" aria-hidden="true">
<span class="compare-handle-grip">
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M15 6l-6 6 6 6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M9 6l6 6-6 6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</div>
</div>

<input
type="range"
class="compare-range"
min="0"
max="100"
value="50"
step="0.1"
aria-label="Comparison position for darkroom grade example"
/>
</div>
</section>

<!-- ============ SIGNATURE: COMPARISON SLIDER 2 ============ -->
<section class="demo-block" aria-labelledby="demo-2-title">
<div class="demo-head">
<h2 id="demo-2-title" class="demo-title">Interface redesign</h2>
<span class="demo-tag">UI mockup · wireframe to shipped</span>
</div>

<div class="compare" style="--ratio: 16/10">
<div class="compare-frame">
<!-- BEFORE layer -->
<div class="compare-layer compare-layer--before">
<div class="scene scene--ui scene--ui-before">
<div class="ui-topbar">
<span class="ui-dot"></span><span class="ui-dot"></span><span class="ui-dot"></span>
</div>
<div class="ui-body">
<div class="ui-side">
<div class="ui-line ui-line--sm"></div>
<div class="ui-line ui-line--sm"></div>
<div class="ui-line ui-line--sm"></div>
</div>
<div class="ui-main">
<div class="ui-line ui-line--lg"></div>
<div class="ui-line ui-line--md"></div>
<div class="ui-cards">
<div class="ui-card"></div>
<div class="ui-card"></div>
<div class="ui-card"></div>
</div>
</div>
</div>
</div>
<span class="scene-label scene-label--before">Before</span>
</div>

<!-- AFTER layer -->
<div class="compare-layer compare-layer--after">
<div class="scene scene--ui scene--ui-after">
<div class="ui-topbar ui-topbar--after">
<span class="ui-dot ui-dot--after"></span><span class="ui-dot ui-dot--after"></span><span class="ui-dot ui-dot--after"></span>
</div>
<div class="ui-body">
<div class="ui-side ui-side--after">
<div class="ui-line ui-line--sm ui-line--after"></div>
<div class="ui-line ui-line--sm ui-line--after-accent"></div>
<div class="ui-line ui-line--sm ui-line--after"></div>
</div>
<div class="ui-main">
<div class="ui-line ui-line--lg ui-line--after"></div>
<div class="ui-line ui-line--md ui-line--after"></div>
<div class="ui-cards">
<div class="ui-card ui-card--after"></div>
<div class="ui-card ui-card--after"></div>
<div class="ui-card ui-card--after"></div>
</div>
</div>
</div>
</div>
<span class="scene-label scene-label--after">After</span>
</div>

<!-- Handle -->
<div class="compare-handle" aria-hidden="true">
<span class="compare-handle-grip">
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M15 6l-6 6 6 6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M9 6l6 6-6 6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</div>
</div>

<input
type="range"
class="compare-range"
min="0"
max="100"
value="35"
step="0.1"
aria-label="Comparison position for interface redesign example"
/>
</div>
</section>

<!-- ============ HOW IT WORKS ============ -->
<section class="how">
<h2 class="section-title">How the reveal works</h2>
<div class="steps">
<div class="step">
<span class="step-no">1</span>
<h3>One custom property</h3>
<p>
Every slider sets <code>--clip-offset</code> on its own <code>.compare</code>
container. The "after" layer's <code>clip-path</code> reads that value —
nothing else needs to know it exists.
</p>
</div>
<div class="step">
<span class="step-no">2</span>
<h3>A transparent range input</h3>
<p>
A native <code>&lt;input type="range"&gt;</code> sits on top, fully
transparent except for its thumb. That's what gives you drag, touch, and
keyboard arrow support for free.
</p>
</div>
<div class="step">
<span class="step-no">3</span>
<h3>One line of JS</h3>
<p>
On <code>input</code>, <code>script.js</code> writes the range's value
straight to the CSS variable. No drag math, no pointer tracking, no
animation frame loop.
</p>
</div>
</div>
</section>

<!-- ============ COPY-READY SNIPPET ============ -->
<section class="snippet-section">
<h2 class="section-title">Copy-ready markup</h2>
<div class="snippet-card">
<div class="snippet-head">
<span class="file-name">index.html</span>
<button class="copy-btn" onclick="copySnippet('snippet-markup', this)">Copy</button>
</div>
<pre class="code" id="snippet-markup"><code>&lt;div class="compare" style="--clip-offset: 50%"&gt;
&lt;div class="compare-frame"&gt;
&lt;div class="compare-layer compare-layer--before"&gt;...&lt;/div&gt;
&lt;div class="compare-layer compare-layer--after"&gt;...&lt;/div&gt;
&lt;div class="compare-handle"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;input type="range" class="compare-range" min="0" max="100" value="50" /&gt;
&lt;/div&gt;</code></pre>
</div>
</section>

<!-- ============ CHECKLIST ============ -->
<section class="checklist">
<h2 class="section-title">Acceptance checklist</h2>
<ul class="check-list">
<li>
<span class="check-mark">✓</span>
<div><strong>Dragging the divider</strong> reveals or hides the top layer smoothly, driven purely by <code>clip-path</code>.</div>
</li>
<li>
<span class="check-mark">✓</span>
<div><strong>Touch gestures work natively</strong> — the handle is a real range input, so mobile drag "just works."</div>
</li>
<li>
<span class="check-mark">✓</span>
<div><strong>Left / Right arrow keys</strong> adjust the slider in 1% steps once the handle is focused.</div>
</li>
<li>
<span class="check-mark">✓</span>
<div><strong>Aspect ratio is fixed per instance</strong> via <code>--ratio</code>, so neither layer distorts as the slider moves.</div>
</li>
</ul>
</section>

<footer class="footer">
<p>
Part of the <strong>EaseMotion CSS</strong> examples submissions ·
<a href="https://github.com/SAPTARSHI-coder/EaseMotion-css">back to the repo</a>
</p>
</footer>
</main>

<script src="script.js"></script>
</body>
</html>
47 changes: 47 additions & 0 deletions submissions/examples/before-after-image-slider-em/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Binds every .compare-range input to its parent .compare container's
// --clip-offset custom property. That single variable drives the
// clip-path on the "after" layer and the handle's position in CSS —
// this file has no drag math, no pointer tracking, no animation loop.

function initComparisonSliders() {
const ranges = document.querySelectorAll(".compare-range");

ranges.forEach((range) => {
const container = range.closest(".compare");
if (!container) return;

const update = () => {
container.style.setProperty("--clip-offset", `${range.value}%`);
};

// Initial paint from the input's starting value attribute.
update();

// Mouse drag, touch drag, and keyboard arrows all fire native
// "input" events on a range control — one listener covers all three.
range.addEventListener("input", update);
});
}

function copySnippet(id, btn) {
const el = document.getElementById(id);
if (!el) return;
const text = el.innerText;

navigator.clipboard
.writeText(text)
.then(() => {
const original = btn.textContent;
btn.textContent = "Copied";
btn.classList.add("is-copied");
setTimeout(() => {
btn.textContent = original;
btn.classList.remove("is-copied");
}, 1500);
})
.catch(() => {
btn.textContent = "Select & copy";
});
}

document.addEventListener("DOMContentLoaded", initComparisonSliders);
Loading
Loading