{
  "id": "motion:motion-timeline-fill",
  "slug": "motion-timeline-fill",
  "kind": "motion",
  "title": {
    "ja": "タイムライン",
    "en": "Timeline progress fill"
  },
  "aiName": "Timeline progress fill",
  "summary": {
    "ja": "縦線が伸び、各段が灯る",
    "en": "A vertical line grows down the page and each step lights up as it reaches it"
  },
  "number": 35,
  "group": "lp-section",
  "techniques": [
    "scroll-progress",
    "css-transition"
  ],
  "rules": [
    {
      "ja": "線は --p に比例して伸び、先端より下の段は灯らない",
      "en": "The fill grows in proportion to --p, and steps below its leading edge stay unlit"
    },
    {
      "ja": "段が灯るのは線が中央を越えた後だけで、先に灯らない",
      "en": "A step lights only after the line has passed its centre, never before"
    },
    {
      "ja": "375px では線と段のノードを左端に寄せ、横スクロールを出さない",
      "en": "At 375px the track and nodes sit at the left edge with no horizontal scroll"
    },
    {
      "ja": "動きを減らす設定では線を全長で、全段を灯した状態で止める",
      "en": "With reduced motion, the line stays at full length and every step stays lit"
    }
  ],
  "reproduction": {
    "prompt": {
      "ja": "タイムライン — Timeline progress fill\n\n縦のタイムラインを作る。段は 5 つで、各段は画面の高さの 40vh を占める。背景の細い線（幅 2px）の上に、同じ太さの線をアクセント色で重ね、その線を `transform: scaleY(var(--p))` で上から下へ伸ばす。--p は 0〜1 で、区画全体のスクロール量から受け取る。\n線の先端が段の中央（区画の高さの (i + 0.5) / 5 の位置）を越えたら、その段の丸いノードに `.is-on` を付けて、白い地から塗りつぶしへ 0.7 秒で色を変える。段のノードは線の左側（375px では左端）に並べ、本文は線の右側に置く。\n幅 720px 以上では線を中央に置いて段の本文を左右に交互に配し、375px では線を左端に寄せて一列にする。\nCSS と素の JavaScript だけで実装し、ライブラリは使わない。\n\n外すと崩れる点:\n- 線は --p に比例して伸び、先端より下の段は灯らない\n- 段が灯るのは線が中央を越えた後だけで、先に灯らない\n- 375px では線と段のノードを左端に寄せ、横スクロールを出さない\n- 動きを減らす設定では線を全長で、全段を灯した状態で止める\n\n動く実物（1 ファイルで完結する HTML）: https://design-vault.pages.dev/designs/motion-timeline-fill/demo.html",
      "en": "Timeline progress fill — Timeline progress fill\n\nBuild a vertical timeline of five steps, each occupying 40vh of the viewport height. A thin 2px track sits behind an accent-coloured fill of the same width, and the fill grows from top to bottom with `transform: scaleY(var(--p))`, where --p runs from 0 to 1 over the scroll progress of the whole section.\nWhen the fill's leading edge passes the centre of step i (at (i + 0.5) / 5 of the section height), add `.is-on` to that step's round node, switching it from the ground colour to the accent over 0.7 seconds.\nAbove 720px, place the line at the centre and alternate the step text left and right; at 375px, pull the line to the left edge and stack the text in one column.\nImplement with CSS and plain JavaScript only; do not use a library.\n\nDo not drop:\n- The fill grows in proportion to --p, and steps below its leading edge stay unlit\n- A step lights only after the line has passed its centre, never before\n- At 375px the track and nodes sit at the left edge with no horizontal scroll\n- With reduced motion, the line stays at full length and every step stays lit\n\nA working example (one self-contained HTML file): https://design-vault.pages.dev/designs/motion-timeline-fill/demo.html"
    },
    "code": {
      "url": "https://design-vault.pages.dev/designs/motion-timeline-fill/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>35 タイムライン — Timeline progress fill</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.tl { position: relative; height: 200vh; }\n.tl-track, .tl-fill {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 11px;\n  width: 2px;\n}\n.tl-track { background: var(--rule); }\n.tl-fill {\n  background: var(--accent);\n  transform-origin: top;\n  transform: scaleY(var(--p, 0));\n}\n.tl-step {\n  position: relative;\n  height: 40vh;\n  display: grid;\n  align-content: center;\n  padding-inline-start: var(--s6);\n}\n.tl-body { display: grid; gap: var(--s2); }\n.tl-node {\n  position: absolute;\n  left: 0;\n  top: 50%;\n  width: 24px;\n  height: 24px;\n  margin-top: -12px;\n  border-radius: 50%;\n  border: 2px solid var(--rule);\n  background: var(--ground);\n  transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease);\n}\n.tl-node.is-on { background: var(--accent); border-color: var(--accent); }\n\n@media (min-width: 720px) {\n  .tl-track, .tl-fill { left: 50%; margin-left: -1px; }\n  .tl-step { padding-inline-start: 0; grid-template-columns: 1fr 1fr; column-gap: var(--s6); }\n  .tl-step .tl-body { grid-column: 1; }\n  .tl-step:nth-child(even) .tl-body { grid-column: 2; }\n  .tl-step:nth-child(odd) .tl-body { text-align: end; }\n  .tl-node { left: 50%; margin-left: -12px; }\n}\n\nhtml:not(.js) .tl-fill { transform: none; }\n@media (prefers-reduced-motion: reduce) {\n  .tl-fill { transform: none; }\n  .tl-node { transition: none; }\n}\n</style>\n</head>\n<body data-motion-number=\"35\">\n<header class=\"dv-label\">\n  <div class=\"dv-wrap\">\n    <p class=\"dv-num\">35</p>\n    <h1 class=\"dv-name\">タイムライン</h1>\n    <p class=\"dv-ai\">Timeline progress fill</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\">\n    <ol class=\"tl\" data-progress=\"through\">\n      <span class=\"tl-track\" aria-hidden=\"true\"></span>\n      <span class=\"tl-fill\" aria-hidden=\"true\"></span>\n      <li class=\"tl-step\">\n        <span class=\"tl-node\" aria-hidden=\"true\"></span>\n        <div class=\"tl-body\">\n          <p class=\"dv-eyebrow\">Step 01</p>\n          <h2 class=\"dv-h2\">見出しが入ります</h2>\n          <p class=\"dv-lead\">本文のダミーです。線が届いた段から灯ります。</p>\n        </div>\n      </li>\n      <li class=\"tl-step\">\n        <span class=\"tl-node\" aria-hidden=\"true\"></span>\n        <div class=\"tl-body\">\n          <p class=\"dv-eyebrow\">Step 02</p>\n          <h2 class=\"dv-h2\">見出しが入ります</h2>\n          <p class=\"dv-lead\">本文のダミーです。段が増えても同じ線を進みます。</p>\n        </div>\n      </li>\n      <li class=\"tl-step\">\n        <span class=\"tl-node\" aria-hidden=\"true\"></span>\n        <div class=\"tl-body\">\n          <p class=\"dv-eyebrow\">Step 03</p>\n          <h2 class=\"dv-h2\">見出しが入ります</h2>\n          <p class=\"dv-lead\">本文のダミーです。中ほどの段です。</p>\n        </div>\n      </li>\n      <li class=\"tl-step\">\n        <span class=\"tl-node\" aria-hidden=\"true\"></span>\n        <div class=\"tl-body\">\n          <p class=\"dv-eyebrow\">Step 04</p>\n          <h2 class=\"dv-h2\">見出しが入ります</h2>\n          <p class=\"dv-lead\">本文のダミーです。最後から二番目の段です。</p>\n        </div>\n      </li>\n      <li class=\"tl-step\">\n        <span class=\"tl-node\" aria-hidden=\"true\"></span>\n        <div class=\"tl-body\">\n          <p class=\"dv-eyebrow\">Step 05</p>\n          <h2 class=\"dv-h2\">見出しが入ります</h2>\n          <p class=\"dv-lead\">本文のダミーです。最後の段に届くと、線が全長になります。</p>\n        </div>\n      </li>\n    </ol>\n  </div>\n</main>\n\n<footer class=\"dv-end\">END — 35 タイムライン</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// 線の先端が段の中央（(i + 0.5) / 5）を越えたら、その段のノードを灯す。\ndvProgress((el, p) => {\n  el.querySelectorAll('.tl-node').forEach((node, i) => {\n    node.classList.toggle('is-on', dvReduce || p >= (i + 0.5) / 5);\n  });\n});\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-timeline-fill",
    "en": "https://design-vault.pages.dev/en/designs/motion-timeline-fill",
    "json": "https://design-vault.pages.dev/ai/assets/motion-timeline-fill.json",
    "designJson": null
  }
}