{
  "id": "motion:motion-compare-wipe",
  "slug": "motion-compare-wipe",
  "kind": "motion",
  "title": {
    "ja": "ビフォーアフター",
    "en": "Before/after wipe"
  },
  "aiName": "Before/after wipe",
  "summary": {
    "ja": "仕切りが動いて前後が入れ替わる",
    "en": "A divider sweeps across and swaps the before and after images"
  },
  "number": 30,
  "group": "media-depth",
  "techniques": [
    "clip-path",
    "position-sticky",
    "scroll-progress"
  ],
  "rules": [
    {
      "ja": "上の図は仕切りの左側だけを見せ、切り抜きで入れ替える。拡大はしない",
      "en": "The top image shows only the left of the divider and swaps by clipping, not scaling"
    },
    {
      "ja": "仕切りは区画の中で動き、図を収める箱は sticky で画面に留まる",
      "en": "The divider moves inside the pinned section; the frame stays fixed on screen"
    },
    {
      "ja": "動きを減らす設定では、後の図を全面に出して止める",
      "en": "With reduced motion, show the after image in full and stop"
    }
  ],
  "reproduction": {
    "prompt": {
      "ja": "ビフォーアフター — Before/after wipe\n\n画面に固定した 2 枚の図（CSS の図形）を重ね、「前」の図を下、「後」の図を上に置く。仕切り線とつまみは、区画の高さ 300vh のスクロールに合わせて左から右へ動く。\n上の図は `clip-path: inset(0 calc((1 - var(--p)) * 100%) 0 0)` で仕切りの左側だけを見せ、--p が 0 のとき前、1 のとき後が全面に出る。\n図を収める箱は 100vh の position: sticky にし、「後」の札は上の図の左端、「前」の札は下の図の右端に置く。\nCSS と素の JavaScript だけで実装し、ライブラリは使わない。\n\n外すと崩れる点:\n- 上の図は仕切りの左側だけを見せ、切り抜きで入れ替える。拡大はしない\n- 仕切りは区画の中で動き、図を収める箱は sticky で画面に留まる\n- 動きを減らす設定では、後の図を全面に出して止める\n\n動く実物（1 ファイルで完結する HTML）: https://design-vault.pages.dev/designs/motion-compare-wipe/demo.html",
      "en": "Before/after wipe — Before/after wipe\n\nStack two CSS-drawn images on the screen: the \"before\" image underneath and the \"after\" image on top. A divider line and its handle travel from left to right as the user scrolls through a 300vh section.\nThe top image uses `clip-path: inset(0 calc((1 - var(--p)) * 100%) 0 0)` to show only the left of the divider, so at --p 0 the before image fills the frame and at --p 1 the after image does.\nHold the frame in a 100vh position: sticky box, put the \"after\" tag at the left edge of the top image and the \"before\" tag at the right edge of the bottom image.\nImplement with CSS and plain JavaScript only; do not use a library.\n\nDo not drop:\n- The top image shows only the left of the divider and swaps by clipping, not scaling\n- The divider moves inside the pinned section; the frame stays fixed on screen\n- With reduced motion, show the after image in full and stop\n\nA working example (one self-contained HTML file): https://design-vault.pages.dev/designs/motion-compare-wipe/demo.html"
    },
    "code": {
      "url": "https://design-vault.pages.dev/designs/motion-compare-wipe/demo.html",
      "html": "<!doctype html>\n<html lang=\"ja\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>30 ビフォーアフター — Before/after wipe</title>\n<style>\n/* dv-motion-base:start */\n:root {\n  --ground: #FFFFFF;\n  --surface: #F3F5F8;\n  --ink: #14181F;\n  --muted: #5A6472;\n  --accent: #1F5FA8;\n  --rule: #D8DDE4;\n  --accent-soft: color-mix(in srgb, var(--accent) 12%, var(--ground));\n  --accent-mid: color-mix(in srgb, var(--accent) 45%, var(--ground));\n  --shade: color-mix(in srgb, var(--ink) 14%, transparent);\n\n  --font: \"Zen Kaku Gothic New\", \"Hiragino Sans\", \"Noto Sans JP\", \"Yu Gothic\", system-ui, sans-serif;\n  --mono: ui-monospace, \"SF Mono\", Menlo, Consolas, monospace;\n\n  --t-label: 13px;\n  --t-body: 16px;\n  --t-lead: 20px;\n  --t-h2: 32px;\n  --t-h1: clamp(40px, 7vw, 72px);\n\n  --s1: 4px;\n  --s2: 8px;\n  --s3: 16px;\n  --s4: 24px;\n  --s5: 40px;\n  --s6: 64px;\n  --s7: 104px;\n\n  --radius: 12px;\n  --ease: cubic-bezier(.2, .7, .2, 1);\n  --dur: .7s;\n  --measure: 1080px;\n}\n*, *::before, *::after { box-sizing: border-box; }\nhtml { -webkit-text-size-adjust: 100%; }\nbody {\n  margin: 0;\n  background: var(--ground);\n  color: var(--ink);\n  font-family: var(--font);\n  font-size: var(--t-body);\n  line-height: 1.7;\n  font-feature-settings: \"palt\";\n  overflow-x: hidden;\n}\nh1, h2, h3, p, ul, ol, figure { margin: 0; padding: 0; }\nul, ol { list-style: none; }\n.dv-wrap { width: min(100% - 2 * var(--s4), var(--measure)); margin-inline: auto; }\n.dv-label {\n  min-height: 72vh;\n  display: grid;\n  align-content: center;\n  gap: var(--s3);\n  padding-block: var(--s6);\n  border-bottom: 1px solid var(--rule);\n}\n.dv-num { font-family: var(--mono); font-size: var(--t-lead); font-weight: 700; color: var(--accent); letter-spacing: .04em; }\n.dv-name { font-size: var(--t-h1); font-weight: 700; line-height: 1.15; letter-spacing: .01em; }\n.dv-ai { font-family: var(--mono); font-size: var(--t-lead); color: var(--muted); }\n.dv-desc { font-size: var(--t-lead); color: var(--ink); }\n.dv-hint { font-size: var(--t-label); color: var(--muted); letter-spacing: .12em; margin-top: var(--s4); }\n.dv-stage { padding-block: var(--s7); }\n.dv-h2 { font-size: var(--t-h2); font-weight: 700; line-height: 1.3; }\n.dv-lead { font-size: var(--t-lead); color: var(--muted); }\n.dv-eyebrow { font-family: var(--mono); font-size: var(--t-label); color: var(--accent); letter-spacing: .12em; text-transform: uppercase; }\n.dv-card { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: var(--s4); }\n.dv-ph {\n  border-radius: var(--radius);\n  background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid) 60%, var(--accent));\n  aspect-ratio: 16 / 9;\n}\n.dv-ph--alt { background: linear-gradient(200deg, var(--surface), var(--accent-soft) 55%, var(--accent-mid)); }\n.dv-ph--ink { background: linear-gradient(160deg, var(--ink), color-mix(in srgb, var(--ink) 55%, var(--accent))); }\n.dv-end {\n  min-height: 60vh;\n  display: grid;\n  place-items: center;\n  border-top: 1px solid var(--rule);\n  color: var(--muted);\n  font-family: var(--mono);\n  font-size: var(--t-label);\n  letter-spacing: .12em;\n}\n/* dv-motion-base:end */\n\n/* ---- この演出だけの CSS。色は var(--…) か color-mix() のみ ---- */\n.stack { display: grid; gap: var(--s6); }\n.js [data-reveal] { opacity: 0; transform: translateY(24px); transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease); }\n.js [data-reveal].is-in { opacity: 1; transform: none; }\n\n/* 区画の高さ 300vh。中の箱は 100vh で画面に留まり、--p で仕切りを動かす */\n.cmp { height: 300vh; }\n.cmp-stick { position: sticky; top: 0; height: 100vh; display: grid; place-items: center; overflow: hidden; }\n.cmp-frame {\n  position: relative;\n  width: min(100vw - 2 * var(--s5), 960px);\n  aspect-ratio: 16 / 9;\n  box-shadow: 0 24px 60px -24px var(--shade);\n  border-radius: var(--radius);\n}\n.cmp-fig { position: absolute; inset: 0; margin: 0; border-radius: var(--radius); overflow: hidden; }\n.cmp-fig--before { background: var(--surface); }\n.cmp-fig--after {\n  background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid) 60%, var(--accent));\n  clip-path: inset(0 calc((1 - var(--p)) * 100%) 0 0);\n}\n.cmp-art { position: absolute; inset: 0; padding: var(--s6) var(--s5) var(--s5); display: grid; gap: var(--s3); align-content: start; }\n.cmp-bar { height: 14px; border-radius: 999px; background: var(--rule); }\n.cmp-fig--after .cmp-bar { background: color-mix(in srgb, var(--ground) 70%, transparent); }\n.cmp-bar:nth-child(2) { width: 62%; }\n.cmp-bar:nth-child(3) { width: 80%; }\n.cmp-bar:nth-child(4) { width: 46%; }\n.cmp-tag {\n  position: absolute; top: var(--s3);\n  padding: var(--s1) var(--s3);\n  border-radius: 999px;\n  font-size: var(--t-label);\n  font-weight: 700;\n}\n.cmp-tag--before { right: var(--s3); background: var(--ground); color: var(--ink); }\n.cmp-tag--after { left: var(--s3); background: var(--ink); color: var(--ground); }\n.cmp-track { position: absolute; inset: 0; transform: translateX(calc(var(--p) * 100%)); pointer-events: none; }\n.cmp-line { position: absolute; top: 0; bottom: 0; left: -1px; width: 2px; background: var(--ground); }\n.cmp-knob {\n  position: absolute; top: 50%; left: 0;\n  width: 48px; height: 48px; margin: -24px 0 0 -24px;\n  border-radius: 50%;\n  background: var(--ground) linear-gradient(var(--ink), var(--ink)) center / 2px 16px no-repeat;\n  box-shadow: 0 6px 20px var(--shade);\n}\n\n@media (max-width: 600px) {\n  .cmp-frame { aspect-ratio: 4 / 3; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .js [data-reveal] { opacity: 1; transform: none; transition: none; }\n  .cmp-fig--after { clip-path: none; }\n  .cmp-track { display: none; }\n}\n</style>\n</head>\n<body data-motion-number=\"30\">\n<header class=\"dv-label\">\n  <div class=\"dv-wrap\">\n    <p class=\"dv-num\">30</p>\n    <h1 class=\"dv-name\">ビフォーアフター</h1>\n    <p class=\"dv-ai\">Before/after wipe</p>\n    <p class=\"dv-desc\">仕切りが動いて前後が入れ替わる</p>\n    <p class=\"dv-hint\">SCROLL ↓</p>\n  </div>\n</header>\n\n<main class=\"dv-stage\">\n  <div class=\"dv-wrap stack\">\n    <section class=\"dv-card\" data-reveal>\n      <p class=\"dv-eyebrow\">Section 1</p>\n      <h2 class=\"dv-h2\">見出しが入ります</h2>\n      <p class=\"dv-lead\">本文のダミーです。スクロールすると仕切りが右へ動き、前の図が後の図に入れ替わります。</p>\n    </section>\n  </div>\n\n  <section class=\"cmp\" data-progress=\"pin\">\n    <div class=\"cmp-stick\">\n      <div class=\"cmp-frame\">\n        <figure class=\"cmp-fig cmp-fig--before\">\n          <span class=\"cmp-tag cmp-tag--before\">前</span>\n          <div class=\"cmp-art\">\n            <div class=\"cmp-bar\"></div>\n            <div class=\"cmp-bar\"></div>\n            <div class=\"cmp-bar\"></div>\n            <div class=\"cmp-bar\"></div>\n          </div>\n        </figure>\n        <figure class=\"cmp-fig cmp-fig--after\">\n          <span class=\"cmp-tag cmp-tag--after\">後</span>\n          <div class=\"cmp-art\">\n            <div class=\"cmp-bar\"></div>\n            <div class=\"cmp-bar\"></div>\n            <div class=\"cmp-bar\"></div>\n            <div class=\"cmp-bar\"></div>\n          </div>\n        </figure>\n        <div class=\"cmp-track\" aria-hidden=\"true\">\n          <span class=\"cmp-line\"></span>\n          <span class=\"cmp-knob\"></span>\n        </div>\n      </div>\n    </div>\n  </section>\n\n  <div class=\"dv-wrap stack\">\n    <section class=\"dv-card\" data-reveal>\n      <p class=\"dv-eyebrow\">Section 2</p>\n      <h2 class=\"dv-h2\">見出しが入ります</h2>\n      <p class=\"dv-lead\">本文のダミーです。仕切りが端まで届いたあとに、この段落が続きます。</p>\n    </section>\n  </div>\n</main>\n\n<footer class=\"dv-end\">END — 30 ビフォーアフター</footer>\n\n<script>\n/* dv-motion-helpers:start */\nconst dvReduce = matchMedia('(prefers-reduced-motion: reduce)').matches;\ndocument.documentElement.classList.add('js');\n\n/**\n * 画面に入った要素へ .is-in を付ける。\n * data-reveal=\"repeat\" の要素は画面を出たら外す（何度でも再生）。\n */\nfunction dvReveal(selector = '[data-reveal]', { threshold = 0.2, rootMargin = '0px 0px -8% 0px' } = {}) {\n  const els = [...document.querySelectorAll(selector)];\n  if (dvReduce || !('IntersectionObserver' in window)) {\n    els.forEach((el) => el.classList.add('is-in'));\n    return;\n  }\n  const io = new IntersectionObserver((entries) => {\n    for (const e of entries) {\n      if (e.isIntersecting) {\n        e.target.classList.add('is-in');\n        if (e.target.dataset.reveal !== 'repeat') io.unobserve(e.target);\n      } else if (e.target.dataset.reveal === 'repeat') {\n        e.target.classList.remove('is-in');\n      }\n    }\n  }, { threshold, rootMargin });\n  els.forEach((el) => io.observe(el));\n}\n\n/**\n * スクロール量を 0〜1 にして、各 [data-progress] 要素の CSS 変数 --p に書く。\n *   data-progress=\"through\" : 要素の上端が画面の下端に来たとき 0、下端が画面の上端を抜けたとき 1\n *   data-progress=\"pin\"     : 要素の上端が画面の上端に来たとき 0、下端が画面の下端に来たとき 1\n *                             （中に position: sticky の箱を持つ、背の高い区画用）\n *   data-progress=\"page\"    : ページ全体のスクロール量\n * onUpdate(el, p) を渡すと、値が変わるたびに呼ばれる。\n */\nfunction dvProgress(onUpdate) {\n  const els = [...document.querySelectorAll('[data-progress]')];\n  const clamp = (n) => Math.min(1, Math.max(0, n));\n  let queued = false;\n  const measure = () => {\n    queued = false;\n    const vh = window.innerHeight;\n    for (const el of els) {\n      const r = el.getBoundingClientRect();\n      let p;\n      if (el.dataset.progress === 'pin') p = r.height > vh ? -r.top / (r.height - vh) : 0;\n      else if (el.dataset.progress === 'page') {\n        const max = document.documentElement.scrollHeight - vh;\n        p = max > 0 ? window.scrollY / max : 0;\n      } else p = (vh - r.top) / (vh + r.height);\n      p = clamp(p);\n      el.style.setProperty('--p', p.toFixed(4));\n      if (onUpdate) onUpdate(el, p);\n    }\n  };\n  const request = () => {\n    if (!queued) { queued = true; requestAnimationFrame(measure); }\n  };\n  addEventListener('scroll', request, { passive: true });\n  addEventListener('resize', request);\n  measure();\n}\n/* dv-motion-helpers:end */\n\n/* ---- この演出だけの JS ---- */\ndvReveal();\ndvProgress();\n</script>\n</body>\n</html>\n"
    }
  },
  "license": "Original",
  "catalogVersion": 1,
  "designSchemaVersion": 2,
  "capabilities": [
    "live-preview",
    "copy-prompt",
    "get-code"
  ],
  "reusePolicy": "construction-reuse",
  "urls": {
    "ja": "https://design-vault.pages.dev/designs/motion-compare-wipe",
    "en": "https://design-vault.pages.dev/en/designs/motion-compare-wipe",
    "json": "https://design-vault.pages.dev/ai/assets/motion-compare-wipe.json",
    "designJson": null
  }
}