diff --git a/submissions/examples/before-after-image-slider-em/README.md b/submissions/examples/before-after-image-slider-em/README.md new file mode 100644 index 00000000000..9eb2d10003b --- /dev/null +++ b/submissions/examples/before-after-image-slider-em/README.md @@ -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 +
+
+
...before content...
+
...after content...
+
+
+ +
+``` + +```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 `` 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. diff --git a/submissions/examples/before-after-image-slider-em/demo.html b/submissions/examples/before-after-image-slider-em/demo.html new file mode 100644 index 00000000000..6b48d3d0a04 --- /dev/null +++ b/submissions/examples/before-after-image-slider-em/demo.html @@ -0,0 +1,256 @@ + + + + + + Before / After Comparison Slider — EaseMotion CSS + + + + +
+ +
+

submissions/examples · before-after-image-slider-em

+

+ Same frame.
+ Two exposures. +

+

+ One slider, one CSS custom property. Drag the grip, use the arrow keys, or touch + and swipe — --clip-offset does the rest, no JavaScript animation + loop required. +

+
+ + +
+
+

Darkroom grade

+ landscape · color grade +
+ +
+
+ +
+ + Before +
+ + +
+ + After +
+ + + +
+ + +
+
+ + +
+
+

Interface redesign

+ UI mockup · wireframe to shipped +
+ +
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Before +
+ + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ After +
+ + + +
+ + +
+
+ + +
+

How the reveal works

+
+
+ 1 +

One custom property

+

+ Every slider sets --clip-offset on its own .compare + container. The "after" layer's clip-path reads that value — + nothing else needs to know it exists. +

+
+
+ 2 +

A transparent range input

+

+ A native <input type="range"> sits on top, fully + transparent except for its thumb. That's what gives you drag, touch, and + keyboard arrow support for free. +

+
+
+ 3 +

One line of JS

+

+ On input, script.js writes the range's value + straight to the CSS variable. No drag math, no pointer tracking, no + animation frame loop. +

+
+
+
+ + +
+

Copy-ready markup

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

Acceptance checklist

+ +
+ + +
+ + + + diff --git a/submissions/examples/before-after-image-slider-em/script.js b/submissions/examples/before-after-image-slider-em/script.js new file mode 100644 index 00000000000..15f687e6a7c --- /dev/null +++ b/submissions/examples/before-after-image-slider-em/script.js @@ -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); diff --git a/submissions/examples/before-after-image-slider-em/style.css b/submissions/examples/before-after-image-slider-em/style.css new file mode 100644 index 00000000000..36d2bc7ed6b --- /dev/null +++ b/submissions/examples/before-after-image-slider-em/style.css @@ -0,0 +1,561 @@ +/* Fonts */ +@import url("https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap"); + +:root { + --ink: #111316; + --panel: #1a1d21; + --panel-2: #202429; + --line: rgba(255, 255, 255, 0.09); + --text: #f1eee7; + --muted: #9195a0; + + --amber: #e8a33d; + --amber-soft: rgba(232, 163, 61, 0.16); + + --font-display: "Fraunces", serif; + --font-body: "Inter", sans-serif; + --font-mono: "IBM Plex Mono", monospace; + + --radius: 14px; + --ease: cubic-bezier(0.22, 1, 0.36, 1); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: var(--ink); + background-image: radial-gradient(circle at 10% 0%, rgba(232, 163, 61, 0.08), transparent 45%); + color: var(--text); + font-family: var(--font-body); + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +code { + font-family: var(--font-mono); + font-size: 0.9em; + color: var(--amber); + background: var(--amber-soft); + padding: 0.1em 0.4em; + border-radius: 5px; +} + +.wrap { + max-width: 900px; + margin: 0 auto; + padding: 72px 24px 100px; +} + +/* ---------- Hero ---------- */ +.hero { + margin-bottom: 48px; +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + font-family: var(--font-mono); + font-size: 0.76rem; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--muted); + margin-bottom: 20px; +} + +.dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--amber); + box-shadow: 0 0 0 3px var(--amber-soft); +} + +.hero-title { + font-family: var(--font-display); + font-weight: 600; + font-optical-sizing: auto; + font-size: clamp(2.2rem, 5.5vw, 3.4rem); + line-height: 1.1; + letter-spacing: -0.01em; + margin-bottom: 20px; +} + +.hero-title .hl { + color: var(--amber); + font-style: italic; +} + +.hero-sub { + max-width: 58ch; + color: var(--muted); + font-size: 1.03rem; +} + +/* ---------- Demo blocks ---------- */ +.demo-block { + margin-bottom: 56px; +} + +.demo-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; + flex-wrap: wrap; +} + +.demo-title { + font-family: var(--font-display); + font-weight: 600; + font-size: 1.35rem; +} + +.demo-tag { + font-family: var(--font-mono); + font-size: 0.76rem; + color: var(--muted); + letter-spacing: 0.02em; +} + +/* ---------- Comparison slider (signature component) ---------- */ +.compare { + --clip-offset: 50%; + position: relative; +} + +.compare-frame { + position: relative; + width: 100%; + aspect-ratio: var(--ratio, 16/10); + border-radius: var(--radius); + overflow: hidden; + border: 1px solid var(--line); + background: #0a0b0d; + cursor: ew-resize; + user-select: none; +} + +.compare-layer { + position: absolute; + inset: 0; +} + +.compare-layer--after { + clip-path: inset(0 calc(100% - var(--clip-offset)) 0 0); + transition: clip-path 0.05s linear; +} + +.scene { + width: 100%; + height: 100%; + display: block; +} + +.scene-label { + position: absolute; + bottom: 14px; + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.06em; + text-transform: uppercase; + padding: 5px 10px; + border-radius: 6px; + background: rgba(10, 11, 13, 0.55); + backdrop-filter: blur(4px); + color: var(--text); +} + +.scene-label--before { + left: 14px; +} + +.scene-label--after { + right: 14px; +} + +/* Handle */ +.compare-handle { + position: absolute; + top: 0; + bottom: 0; + left: var(--clip-offset); + width: 0; + transform: translateX(-50%); + pointer-events: none; +} + +.compare-handle::before { + content: ""; + position: absolute; + inset: 0; + left: 50%; + width: 2px; + transform: translateX(-50%); + background: rgba(241, 238, 231, 0.85); + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25); +} + +.compare-handle-grip { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 38px; + height: 38px; + border-radius: 50%; + background: var(--text); + color: var(--ink); + display: flex; + align-items: center; + justify-content: center; + gap: 2px; + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35); +} + +/* The real, interactive control: a transparent range input on top */ +.compare-range { + position: absolute; + inset: 0; + top: -100%; + height: 300%; + width: 100%; + margin: 0; + appearance: none; + -webkit-appearance: none; + background: transparent; + cursor: ew-resize; +} + +.compare-range:focus-visible { + outline: none; +} + +.compare-range::-webkit-slider-thumb { + -webkit-appearance: none; + width: 38px; + height: 38px; + background: transparent; + border-radius: 50%; + cursor: ew-resize; +} + +.compare-range::-moz-range-thumb { + width: 38px; + height: 38px; + background: transparent; + border: none; + border-radius: 50%; + cursor: ew-resize; +} + +.compare-range::-webkit-slider-runnable-track { + background: transparent; +} + +.compare-range::-moz-range-track { + background: transparent; +} + +.compare:focus-within .compare-handle-grip { + box-shadow: 0 0 0 3px var(--amber-soft), 0 4px 14px rgba(0, 0, 0, 0.35); +} + +/* ---------- UI redesign scene (built from divs, not images) ---------- */ +.scene--ui { + width: 100%; + height: 100%; + padding: 18px; + display: flex; + flex-direction: column; + gap: 14px; +} + +.scene--ui-before { + background: #2b2e33; +} + +.scene--ui-after { + background: linear-gradient(160deg, #241a38, #3d2b52 60%, #6b3f6b); +} + +.ui-topbar { + display: flex; + gap: 6px; +} + +.ui-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #55595f; +} + +.ui-dot--after { + background: var(--amber); +} + +.ui-body { + flex: 1; + display: grid; + grid-template-columns: 70px 1fr; + gap: 12px; +} + +.ui-side { + display: flex; + flex-direction: column; + gap: 8px; +} + +.ui-line { + border-radius: 4px; + background: #3d4148; +} + +.ui-line--after { + background: rgba(241, 238, 231, 0.3); +} + +.ui-line--after-accent { + background: var(--amber); +} + +.ui-line--sm { + height: 8px; + width: 100%; +} + +.ui-line--lg { + height: 16px; + width: 55%; + margin-bottom: 4px; +} + +.ui-line--md { + height: 9px; + width: 80%; + margin-bottom: 10px; +} + +.ui-cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + flex: 1; +} + +.ui-card { + border-radius: 6px; + background: #3d4148; +} + +.ui-card--after { + background: rgba(241, 238, 231, 0.22); + border: 1px solid rgba(232, 163, 61, 0.4); +} + +/* ---------- Shared section styles ---------- */ +.section-title { + font-family: var(--font-display); + font-weight: 600; + font-size: 1.5rem; + margin-bottom: 22px; +} + +section { + margin-bottom: 64px; +} + +/* ---------- How it works ---------- */ +.steps { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +.step { + background: var(--panel); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 20px; +} + +.step-no { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border-radius: 50%; + background: var(--amber-soft); + color: var(--amber); + font-family: var(--font-mono); + font-size: 0.8rem; + font-weight: 600; + margin-bottom: 12px; +} + +.step h3 { + font-family: var(--font-display); + font-weight: 600; + font-size: 1rem; + margin-bottom: 8px; +} + +.step p { + font-size: 0.88rem; + color: var(--muted); +} + +/* ---------- Snippet ---------- */ +.snippet-card { + background: var(--panel); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 16px; +} + +.snippet-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.file-name { + font-family: var(--font-mono); + font-size: 0.86rem; +} + +.copy-btn { + font-family: var(--font-mono); + font-size: 0.74rem; + color: var(--text); + background: var(--panel-2); + border: 1px solid var(--line); + border-radius: 6px; + padding: 5px 11px; + cursor: pointer; + transition: border-color 0.2s var(--ease), color 0.2s var(--ease); +} + +.copy-btn:hover { + border-color: var(--amber); + color: var(--amber); +} + +.copy-btn.is-copied { + border-color: var(--amber); + color: var(--amber); +} + +.code { + font-family: var(--font-mono); + font-size: 0.82rem; + line-height: 1.65; + background: #0a0b0d; + border: 1px solid var(--line); + border-radius: 9px; + padding: 14px 16px; + overflow-x: auto; + color: #cbd0dc; +} + +.code code { + background: none; + color: inherit; + padding: 0; + font-size: 1em; +} + +/* ---------- Checklist ---------- */ +.check-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 12px; +} + +.check-list li { + display: flex; + gap: 12px; + align-items: flex-start; + background: var(--panel); + border: 1px solid var(--line); + border-radius: 10px; + padding: 14px 16px; + font-size: 0.9rem; + color: var(--muted); +} + +.check-mark { + flex-shrink: 0; + width: 20px; + height: 20px; + border-radius: 6px; + background: var(--amber-soft); + color: var(--amber); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.72rem; + font-weight: 700; + margin-top: 2px; +} + +.check-list strong { + color: var(--text); + font-weight: 600; +} + +/* ---------- Footer ---------- */ +.footer { + text-align: center; + color: var(--muted); + font-size: 0.85rem; + padding-top: 24px; + border-top: 1px solid var(--line); +} + +.footer a { + color: var(--amber); + text-decoration: none; +} + +.footer a:hover { + text-decoration: underline; +} + +/* ---------- Responsive ---------- */ +@media (max-width: 720px) { + .wrap { + padding: 48px 18px 72px; + } + + .steps { + grid-template-columns: 1fr; + } + + .ui-body { + grid-template-columns: 50px 1fr; + } +} + +/* ---------- Accessibility ---------- */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} + +:focus-visible { + outline: 2px solid var(--amber); + outline-offset: 2px; +}