{
  "id": "motion:motion-card-flip-3d",
  "slug": "motion-card-flip-3d",
  "kind": "motion",
  "title": {
    "ja": "3D カードフリップ",
    "en": "3D card flip"
  },
  "aiName": "3D card flip on scroll",
  "summary": {
    "ja": "カードが裏返って裏面が出る",
    "en": "A card turns over in 3D to reveal its back"
  },
  "number": 31,
  "group": "media-depth",
  "techniques": [
    "intersection-observer",
    "css-3d",
    "css-transition"
  ],
  "rules": [
    {
      "ja": "裏返しは両面を `backface-visibility: hidden` で隠し、裏の面が表の面の裏側に回り込むように重ねる",
      "en": "Both faces use backface-visibility: hidden and are stacked back to back in the turning box"
    },
    {
      "ja": "一度裏返ったカードは、スクロールで戻っても表に戻さない",
      "en": "Once a card has turned, it stays turned when the user scrolls back"
    },
    {
      "ja": "動きを減らす設定では、裏返さず裏面を見せた状態で止める",
      "en": "With reduced motion, do not turn the cards; show the back face at rest"
    }
  ],
  "reproduction": {
    "prompt": {
      "ja": "3D カードフリップ — 3D card flip on scroll\n\n縦に並んだ 3 枚のカードが、画面の中央に来るたびに裏返る。カードは幅 520px 以下で、表の面は白、裏の面は強調色で、どちらも高さ 320px の角丸。\n裏返しは `rotateY(0 → 180deg)` を 0.7 秒の緩急で行い、親要素に `perspective: 1000px`、裏返す箱に `transform-style: preserve-3d`、両面に `backface-visibility: hidden` を付ける。\nカードは画面の上下 45% を除いた中央の帯に入った時点で一度だけ裏返り、戻らない。3 枚は別々のタイミングで順に裏返る。\n動きを減らす設定では、裏返しを行わず裏面を見せた状態で止める。\nCSS と IntersectionObserver だけで実装し、ライブラリは使わない。\n\n外すと崩れる点:\n- 裏返しは両面を `backface-visibility: hidden` で隠し、裏の面が表の面の裏側に回り込むように重ねる\n- 一度裏返ったカードは、スクロールで戻っても表に戻さない\n- 動きを減らす設定では、裏返さず裏面を見せた状態で止める\n\n動く実物（1 ファイルで完結する HTML）: https://design-vault.pages.dev/designs/motion-card-flip-3d/demo.html",
      "en": "3D card flip — 3D card flip on scroll\n\nThree cards stacked vertically each turn over when they reach the centre of the screen. Each card is at most 520px wide, with a white front face and an accent-coloured back face, both 320px tall with rounded corners.\nTurn each card with `rotateY(0 → 180deg)` over 0.7 seconds with an ease-out curve. Set `perspective: 1000px` on the parent, `transform-style: preserve-3d` on the box that turns, and `backface-visibility: hidden` on both faces.\nA card turns once, when it enters the middle band of the screen (the area excluding the top and bottom 45%), and does not turn back. The three cards turn one after another at different moments.\nWith reduced motion, skip the turn and show the back face at rest.\nImplement with CSS and IntersectionObserver only; do not use a library.\n\nDo not drop:\n- Both faces use backface-visibility: hidden and are stacked back to back in the turning box\n- Once a card has turned, it stays turned when the user scrolls back\n- With reduced motion, do not turn the cards; show the back face at rest\n\nA working example (one self-contained HTML file): https://design-vault.pages.dev/designs/motion-card-flip-3d/demo.html"
    },
    "code": {
      "url": "https://design-vault.pages.dev/designs/motion-card-flip-3d/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>31 3D カードフリップ — 3D card flip on scroll</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.flip-stack { display: grid; gap: 60vh; justify-items: center; padding-block: 30vh; }\n.flip { width: min(100%, 520px); perspective: 1000px; }\n.flip-inner { display: grid; gap: var(--s3); }\n.flip-face {\n  min-height: 320px;\n  display: grid;\n  align-content: center;\n  gap: var(--s2);\n  padding: var(--s4);\n  border-radius: var(--radius);\n  border: 1px solid var(--rule);\n  box-shadow: 0 16px 40px var(--shade);\n  backface-visibility: hidden;\n  -webkit-backface-visibility: hidden;\n}\n.flip-face--back {\n  background: var(--accent);\n  border-color: var(--accent);\n  color: var(--ground);\n  transform: rotateY(180deg);\n}\n.flip-no { font-family: var(--mono); font-size: var(--t-label); color: var(--accent); letter-spacing: .12em; }\n.flip-face--back .flip-no { color: var(--ground); }\n.flip-back-name { font-size: var(--t-h2); font-weight: 700; }\n.js .flip-inner { position: relative; display: block; transform-style: preserve-3d; transition: transform var(--dur) var(--ease); }\n.js .flip-face { position: absolute; inset: 0; }\n.js .flip-face--front { position: relative; }\n.js .flip.is-in .flip-inner { transform: rotateY(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n  .js .flip-inner { transition: none; }\n}\n</style>\n</head>\n<body data-motion-number=\"31\">\n<header class=\"dv-label\">\n  <div class=\"dv-wrap\">\n    <p class=\"dv-num\">31</p>\n    <h1 class=\"dv-name\">3D カードフリップ</h1>\n    <p class=\"dv-ai\">3D card flip on scroll</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\">\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  <div class=\"dv-wrap flip-stack\">\n    <div class=\"flip\" data-flip>\n      <div class=\"flip-inner\">\n        <div class=\"flip-face flip-face--front\">\n          <p class=\"flip-no\">CARD 01</p>\n          <h2 class=\"dv-h2\">見出しが入ります</h2>\n          <p class=\"dv-lead\">表の本文のダミーです。</p>\n        </div>\n        <div class=\"flip-face flip-face--back\">\n          <p class=\"flip-no\">BACK 01</p>\n          <p class=\"flip-back-name\">裏面が入ります</p>\n        </div>\n      </div>\n    </div>\n\n    <div class=\"flip\" data-flip>\n      <div class=\"flip-inner\">\n        <div class=\"flip-face flip-face--front\">\n          <p class=\"flip-no\">CARD 02</p>\n          <h2 class=\"dv-h2\">見出しが入ります</h2>\n          <p class=\"dv-lead\">表の本文のダミーです。</p>\n        </div>\n        <div class=\"flip-face flip-face--back\">\n          <p class=\"flip-no\">BACK 02</p>\n          <p class=\"flip-back-name\">裏面が入ります</p>\n        </div>\n      </div>\n    </div>\n\n    <div class=\"flip\" data-flip>\n      <div class=\"flip-inner\">\n        <div class=\"flip-face flip-face--front\">\n          <p class=\"flip-no\">CARD 03</p>\n          <h2 class=\"dv-h2\">見出しが入ります</h2>\n          <p class=\"dv-lead\">表の本文のダミーです。</p>\n        </div>\n        <div class=\"flip-face flip-face--back\">\n          <p class=\"flip-no\">BACK 03</p>\n          <p class=\"flip-back-name\">裏面が入ります</p>\n        </div>\n      </div>\n    </div>\n  </div>\n\n  <div class=\"dv-wrap stack\">\n    <section class=\"dv-card\">\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 — 31 3D カードフリップ</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 ---- */\n// 中央の帯（画面の上下 45% を除いた部分）に入ったカードへ .is-in を付け、一度だけ裏返す。\ndvReveal('[data-flip]', { threshold: 0, rootMargin: '-45% 0px -45% 0px' });\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-card-flip-3d",
    "en": "https://design-vault.pages.dev/en/designs/motion-card-flip-3d",
    "json": "https://design-vault.pages.dev/ai/assets/motion-card-flip-3d.json",
    "designJson": null
  }
}