Back to index

23 Hide-on-scroll header

Motion · Pinning, paging, navigation
Hide-on-scroll header

A still — with scripts on, it runs right here

Take it with you

Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.

Everything

motion-hide-on-scroll-header-everything.md

Everything in this panel, including the contents behind each URL, in one Markdown file

Prompt

928 B
Hide-on-scroll header — Hide-on-scroll header

Place a fixed 64px header at the top edge. When the user scrolls down, slide it out with translateY(-100%); when they scroll up, bring it back to translateY(0).
Judge direction only after the position has moved more than 8px from the last accepted position, so small jitter never toggles it.
While the reader is still inside the 72vh title block, keep the header hidden in both directions.
Animate over 0.7s with the ease curve, using CSS and a single passive scroll listener; no library.

Do not drop:
- Hide on scroll down and reveal on scroll up, deciding direction only past an 8px threshold
- Stay hidden inside the title block; it first appears only after the title block is passed
- Under reduced motion, never hide it; keep the header visible

A working example (one self-contained HTML file): https://design-vault.pages.dev/designs/motion-hide-on-scroll-header/demo.html

Files

demo.html

9.0 KB

/designs/motion-hide-on-scroll-header/demo.html — One self-contained HTML file. CSS and plain JavaScript only; it fetches nothing.

Show contents
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>23 隠れるヘッダー — Hide-on-scroll header</title>
<style>
/* dv-motion-base:start */
:root {
  --ground: #FFFFFF;
  --surface: #F3F5F8;
  --ink: #14181F;
  --muted: #5A6472;
  --accent: #1F5FA8;
  --rule: #D8DDE4;
  --accent-soft: color-mix(in srgb, var(--accent) 12%, var(--ground));
  --accent-mid: color-mix(in srgb, var(--accent) 45%, var(--ground));
  --shade: color-mix(in srgb, var(--ink) 14%, transparent);

  --font: "Zen Kaku Gothic New", "Hiragino Sans", "Noto Sans JP", "Yu Gothic", system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --t-label: 13px;
  --t-body: 16px;
  --t-lead: 20px;
  --t-h2: 32px;
  --t-h1: clamp(40px, 7vw, 72px);

  --s1: 4px;
  --s2: 8px;
  --s3: 16px;
  --s4: 24px;
  --s5: 40px;
  --s6: 64px;
  --s7: 104px;

  --radius: 12px;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --dur: .7s;
  --measure: 1080px;
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.7;
  font-feature-settings: "palt";
  overflow-x: hidden;
}
h1, h2, h3, p, ul, ol, figure { margin: 0; padding: 0; }
ul, ol { list-style: none; }
.dv-wrap { width: min(100% - 2 * var(--s4), var(--measure)); margin-inline: auto; }
.dv-label {
  min-height: 72vh;
  display: grid;
  align-content: center;
  gap: var(--s3);
  padding-block: var(--s6);
  border-bottom: 1px solid var(--rule);
}
.dv-num { font-family: var(--mono); font-size: var(--t-lead); font-weight: 700; color: var(--accent); letter-spacing: .04em; }
.dv-name { font-size: var(--t-h1); font-weight: 700; line-height: 1.15; letter-spacing: .01em; }
.dv-ai { font-family: var(--mono); font-size: var(--t-lead); color: var(--muted); }
.dv-desc { font-size: var(--t-lead); color: var(--ink); }
.dv-hint { font-size: var(--t-label); color: var(--muted); letter-spacing: .12em; margin-top: var(--s4); }
.dv-stage { padding-block: var(--s7); }
.dv-h2 { font-size: var(--t-h2); font-weight: 700; line-height: 1.3; }
.dv-lead { font-size: var(--t-lead); color: var(--muted); }
.dv-eyebrow { font-family: var(--mono); font-size: var(--t-label); color: var(--accent); letter-spacing: .12em; text-transform: uppercase; }
.dv-card { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: var(--s4); }
.dv-ph {
  border-radius: var(--radius);
  background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid) 60%, var(--accent));
  aspect-ratio: 16 / 9;
}
.dv-ph--alt { background: linear-gradient(200deg, var(--surface), var(--accent-soft) 55%, var(--accent-mid)); }
.dv-ph--ink { background: linear-gradient(160deg, var(--ink), color-mix(in srgb, var(--ink) 55%, var(--accent))); }
.dv-end {
  min-height: 60vh;
  display: grid;
  place-items: center;
  border-top: 1px solid var(--rule);
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: .12em;
}
/* dv-motion-base:end */

/* ---- この演出だけの CSS。色は var(--…) か color-mix() のみ ---- */
.hd-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  height: 64px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding-inline: var(--s4);
  background: color-mix(in srgb, var(--ground) 92%, transparent);
  border-bottom: 1px solid var(--rule);
  transition: transform var(--dur) var(--ease);
}
.hd-brand { font-family: var(--mono); font-weight: 700; color: var(--ink); }
.hd-bar ul { display: flex; gap: var(--s4); font-size: var(--t-label); color: var(--muted); }
.hd-section { min-height: 80vh; display: grid; align-content: center; }
.js .hd-bar.is-hidden { transform: translateY(-100%); }

@media (prefers-reduced-motion: reduce) {
  .hd-bar { transition: none; }
}
</style>
</head>
<body data-motion-number="23">
<header class="dv-label">
  <div class="dv-wrap">
    <p class="dv-num">23</p>
    <h1 class="dv-name">隠れるヘッダー</h1>
    <p class="dv-ai">Hide-on-scroll header</p>
    <p class="dv-desc">下で隠れ、戻すと出る</p>
    <p class="dv-hint">SCROLL ↓</p>
  </div>
</header>

<main class="dv-stage">
  <header class="hd-bar">
    <p class="hd-brand">ロゴ</p>
    <nav aria-label="主要メニュー">
      <ul>
        <li>製品</li>
        <li>事例</li>
        <li>料金</li>
      </ul>
    </nav>
  </header>
  <section class="hd-section">
    <div class="dv-wrap">
      <p class="dv-eyebrow">Section 1</p>
      <h2 class="dv-h2">見出しが入ります</h2>
      <p class="dv-lead">本文のダミーです。下へ進むとバーが隠れ、上へ戻すと現れます。</p>
    </div>
  </section>
  <section class="hd-section">
    <div class="dv-wrap">
      <p class="dv-eyebrow">Section 2</p>
      <h2 class="dv-h2">見出しが入ります</h2>
      <p class="dv-lead">本文のダミーです。下へ進むとバーが隠れ、上へ戻すと現れます。</p>
    </div>
  </section>
  <section class="hd-section">
    <div class="dv-wrap">
      <p class="dv-eyebrow">Section 3</p>
      <h2 class="dv-h2">見出しが入ります</h2>
      <p class="dv-lead">本文のダミーです。下へ進むとバーが隠れ、上へ戻すと現れます。</p>
    </div>
  </section>
  <section class="hd-section">
    <div class="dv-wrap">
      <p class="dv-eyebrow">Section 4</p>
      <h2 class="dv-h2">見出しが入ります</h2>
      <p class="dv-lead">本文のダミーです。下へ進むとバーが隠れ、上へ戻すと現れます。</p>
    </div>
  </section>
</main>

<footer class="dv-end">END — 23 隠れるヘッダー</footer>

<script>
/* dv-motion-helpers:start */
const dvReduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
document.documentElement.classList.add('js');

/**
 * 画面に入った要素へ .is-in を付ける。
 * data-reveal="repeat" の要素は画面を出たら外す(何度でも再生)。
 */
function dvReveal(selector = '[data-reveal]', { threshold = 0.2, rootMargin = '0px 0px -8% 0px' } = {}) {
  const els = [...document.querySelectorAll(selector)];
  if (dvReduce || !('IntersectionObserver' in window)) {
    els.forEach((el) => el.classList.add('is-in'));
    return;
  }
  const io = new IntersectionObserver((entries) => {
    for (const e of entries) {
      if (e.isIntersecting) {
        e.target.classList.add('is-in');
        if (e.target.dataset.reveal !== 'repeat') io.unobserve(e.target);
      } else if (e.target.dataset.reveal === 'repeat') {
        e.target.classList.remove('is-in');
      }
    }
  }, { threshold, rootMargin });
  els.forEach((el) => io.observe(el));
}

/**
 * スクロール量を 0〜1 にして、各 [data-progress] 要素の CSS 変数 --p に書く。
 *   data-progress="through" : 要素の上端が画面の下端に来たとき 0、下端が画面の上端を抜けたとき 1
 *   data-progress="pin"     : 要素の上端が画面の上端に来たとき 0、下端が画面の下端に来たとき 1
 *                             (中に position: sticky の箱を持つ、背の高い区画用)
 *   data-progress="page"    : ページ全体のスクロール量
 * onUpdate(el, p) を渡すと、値が変わるたびに呼ばれる。
 */
function dvProgress(onUpdate) {
  const els = [...document.querySelectorAll('[data-progress]')];
  const clamp = (n) => Math.min(1, Math.max(0, n));
  let queued = false;
  const measure = () => {
    queued = false;
    const vh = window.innerHeight;
    for (const el of els) {
      const r = el.getBoundingClientRect();
      let p;
      if (el.dataset.progress === 'pin') p = r.height > vh ? -r.top / (r.height - vh) : 0;
      else if (el.dataset.progress === 'page') {
        const max = document.documentElement.scrollHeight - vh;
        p = max > 0 ? window.scrollY / max : 0;
      } else p = (vh - r.top) / (vh + r.height);
      p = clamp(p);
      el.style.setProperty('--p', p.toFixed(4));
      if (onUpdate) onUpdate(el, p);
    }
  };
  const request = () => {
    if (!queued) { queued = true; requestAnimationFrame(measure); }
  };
  addEventListener('scroll', request, { passive: true });
  addEventListener('resize', request);
  measure();
}
/* dv-motion-helpers:end */

/* ---- この演出だけの JS ---- */
const hdBar = document.querySelector('.hd-bar');
const hdLabel = document.querySelector('.dv-label');
if (!dvReduce) {
  // 直前に採った位置から 8px 以上動いたときだけ、向きを見て切り替える。
  // 名札の中(名札の高さより上)は、向きに関係なく隠したままにする。
  let lastY = window.scrollY;
  hdBar.classList.add('is-hidden');
  addEventListener('scroll', () => {
    const y = window.scrollY;
    if (Math.abs(y - lastY) < 8) return;
    const down = y > lastY;
    lastY = y;
    hdBar.classList.toggle('is-hidden', down || y < hdLabel.offsetHeight);
  }, { passive: true });
}
</script>
</body>
</html>

/ai/assets/motion-hide-on-scroll-header.json