テック技報最新号へ →
CSS

ナビが溢れたら CSS だけでドロワーに切り替える(scroll-timeline で溢れ検知)

ナビのリストに scroll-timeline を付け、「スクロールできる=溢れている」時だけ有効になるタイムラインでカスタムプロパティを --true に切り替え、スタイルクエリでナビとドロワーボタンを出し分ける。

@tak_dcxi · 2026-10-17
#ナビゲーション#レスポンシブ#scroll-timeline#スタイルクエリ#ドロワー#JSなし

どんな時に使う?

ヘッダーのグローバルナビを「画面幅 ○px 以下ならハンバーガー」とブレイクポイントで切り替えると、CMS で項目が増えたり文言が長くなったりした時に、ブレイクポイントより広い幅でもナビが溢れて崩れる。この方法は「実際に収まらなくなった瞬間」を CSS が検知して切り替えるので、項目数や文字数が変わっても破綻しない。JS の ResizeObserver も要らない。

仕組み

  1. ナビのリストを overflow-inline: auto にして、scroll-timeline: --nav-list inline を付ける。スクロールタイムラインは「実際にスクロールできる時」だけアクティブになり、溢れていない時は非アクティブになる。
  2. ヘッダー側で timeline-scope を使ってそのタイムライン名を親まで引き上げ、animation-timeline: --nav-list でキーフレームを紐付ける。キーフレームは最初から最後までカスタムプロパティを --true にするだけ。
  3. タイムラインが非アクティブ(溢れていない)ならアニメーションは効かず、初期値の --false のまま。溢れた瞬間だけ --true になる。これを「溢れフラグ」として使う。
  4. 子要素で @container style(--flag: --true) を書き、ナビ・お問い合わせリンク・ドロワーボタンの表示を切り替える。

コード

HTML の骨組み(ロゴ、ナビ、右端にドロワーボタンとお問い合わせリンクを重ねて置く列):

html
<header class="site-header">
  <a class="logo" href="/">Logo</a>
  <nav aria-label="グローバルナビゲーション">
    <ul class="nav-list">…</ul>
  </nav>
  <div class="end-column">
    <button class="drawer-button" type="button" aria-label="メニュー">…</button>
    <a class="contact-link" href="#">お問い合わせ</a>
  </div>
</header>
css
/* タイムラインがアクティブ(=溢れている)間だけ --true になる */
@keyframes header-observe {
  from, to { --header-overflowing: --true; }
}

.site-header {
  --header-overflowing: --false;
  display: grid;
  /* 中央のナビ列は 0 まで縮めるので、狭くなるとリストが溢れる */
  grid-template-columns: minmax(max-content, 1fr) minmax(0, auto) minmax(max-content, 1fr);
  timeline-scope: --nav-list;          /* 子のタイムライン名を親から参照可能にする */
  animation: header-observe 1ms both;
  animation-timeline: --nav-list;
}

.nav-list {
  display: grid;
  grid-auto-flow: column;
  overflow-inline: auto;
  scroll-timeline: --nav-list inline;

  @container style(--header-overflowing: --true) {
    visibility: hidden; /* display: none にすると溢れが消えてフラグが戻り、無限ループする */
  }
}

.end-column {
  display: grid;
  & > * { grid-area: 1 / 1; justify-self: end; } /* ボタンとリンクを同じマスに重ねる */
}

.contact-link {
  @container style(--header-overflowing: --true) { visibility: hidden; }
}
.drawer-button {
  @container style(--header-overflowing: --false) { visibility: hidden; }
}

(元の投稿は @scope (.scoped.site-header) to (.scoped) で囲んでコンポーネント内に閉じている。@scope の使い方は @scope の「to (.scoped)」でコンポーネントのスタイル漏れを止める を参照。)

注意点・対応状況

  • 隠すのは必ず visibility: hidden。display: none だとナビが消えて溢れが解消し、フラグが --false に戻ってまた表示され、を繰り返す。visibility: hidden なら場所は確保されたまま溢れ判定が続き、支援技術からも隠れる。
  • 必要な機能はスクロール駆動アニメーション(scroll-timeline / animation-timeline / timeline-scope)とカスタムプロパティのスタイルクエリ。投稿者によると Firefox は 159 から動く。未対応ブラウザではフラグが --false のままなのでナビが常に表示される(溢れたら横スクロールになる)。フォールバックとして許容できるか確認する。
  • ドロワー本体(開いた時のメニュー)は別途 popover や <dialog> で用意する。この技は「どちらを見せるか」の切り替えだけ。
  • prefers-reduced-motion でアニメーションを全部止めるリセット CSS を入れていると、このフラグ用アニメーションまで止まって切り替わらなくなる。除外しておく。

出典

X の元の投稿を開く