スクロールの状態ごとにスタイルを指定するCSS 前編 scroll-state()で判定できる3つの状態

スクロールの状態の変化をCSSだけで判定し、スタイルを定義できるコンテナクエリーについて説明します。

発行

著者 坂巻 翔大郎 フロントエンド・エンジニア
スクロールの状態ごとにスタイルを指定するCSS シリーズの記事一覧

はじめに

この連載ではCSSの新しい仕様のscroll-state()というコンテナクエリーを取り上げます。

コンテナクエリーについておさらいしておくと、「クエリー」とつくことからもわかるように、メディアクエリーのように条件を指定して、その条件に合致する場合に適用されるCSSを指定できます。文書中のある要素を「クエリーコンテナ」と決め、そのコンテナのさまざまな条件(大きさやスタイル)に応じてCSSを書き分けられるようにする仕組みです。CSS Conditional Rules仕様に定義されています。

コンテナクエリーscroll-state()では、コンテナのスクロールに関する3つの状態を判定できます。

  • スクロールがスタックしている状態(要素が端に固定されたとき)
  • スクロールがスナップしている状態(要素が軸にスナップされたとき)
  • スクロールが可能な状態(要素がスクロールできるとき)

ウェブサイトの実装では、スクロールの状態に応じて要素の見た目を変えたい場面は少なくありません。代表例が、stickyヘッダーが画面上部に固定されたときに影を付ける実装です。この例については、以前の記事(固定状態のときだけスタイルを変える | 今日から使えるCSS 第3回 position: sticky)で紹介しています。

従来はJavaScriptでスクロール位置を判定して実装していましたが、scroll-state()の「スクロールがスタックしている状態」の判定を使えば、CSSだけで実現できるようになりました。

なお、scroll-state() は執筆現在、ChromeとEdgeの最新版(133以降)で利用可能です。

今回は、そのscroll-state()の基本的な知識を解説し、次回はそれを活用した実践的なアイデアを紹介します。

scroll-state()の構文

基本の書き方は次のようになります。

.container {
  container-type: scroll-state;
}

@container scroll-state(条件: 検知方法) {
  /* 条件にマッチしたときのスタイルを書く */
}

このscroll-state(条件: 検知方法)クエリーの中には、container-type: scroll-state;が指定された要素の「子要素」のスタイルを書きます。

条件は次の3種類から指定します。

  • stuck:要素が端に固定されたとき
  • snapped:要素が軸にスナップされたとき
  • scrollable:要素がスクロールできるとき

次に、検知方法を、条件の:(コロン)の後ろに指定します。

状態 指定できる値(いずれか1つ)
stuck none top right bottom left block-start inline-start block-end inline-end
snapped none x y block inline
scrollable none top right bottom left block-start inline-start block-end inline-end x y block inline

blockinlineから始まる値は、コンテンツの書字方向に基づく論理的な値です。理解を深めたい方は、論理プロパティを使いこなすシリーズを参考にしてみてください。

たとえば、要素が上方向にスクロール可能なときに文字を赤くしたい場合は次のようにします。

.container {
  container-type: scroll-state;
}

@container scroll-state(scrollable: top) {
  p { color: red; }
}

左側のデモは、縦方向のスクロールを可能にしてあります。少し下方向にスクロールすれば、上方向にスクロールが可能になり条件にマッチするため、文字が赤くなります。

右側は、縦と横方向のスクロールを可能にしてあります。左と同様に、少しでも下方向にスクロールすれば「上方向にスクロール可能」になるため文字は赤くなります。しかし、右方向にスクロールしただけでは、「上方向にスクロール可能」にはならないので、文字は赤くなりません。

ウェブサイトの閲覧時にユーザーがスクロールする場面は多々あり、スクロールに合わせた挙動を加えたいケースはよくあります。構文と簡単な動作だけをみてもいまいちピンと来ないかもしれませんが、記事を読み終わる頃には「なんて便利なんだ...」となっているはずです。

では、それぞれの条件について詳しく見ていきましょう。

要素が端に固定されたときに何かする

position: stickyを使って、スクロールしたときにヘッダーを上に固定するUIは今では良く見られますが、これまでは、ヘッダーが固定されたときにその見た目も変えるなんてことはできませんでした。scroll-state(stuck)を使えば、要素を固定してスタイルを変化させることも簡単に実現できます。

ヘッダーナビゲーションをstickyにして、固定されたときの見た目をすりガラスのようなスタイルに変えてみましょう。

まずはHTMLです。ヘッダーの中にサイト名とメニューがあります。そして文章が続きます。

<div class="container">
  <div class="header">
    <h1>デモサイト</h1>
    <ul>
      <li><a href="#">メニュー1</a></li>
      <li><a href="#">メニュー2</a></li>
      <li><a href="#">メニュー3</a></li>
    </ul>
  </div>
</div>

<p>よそも結果もしそういう反抗者としてののうちを考えだない。</p>
<!-- 省略 -->

次にCSSです。ヘッダーが端に固定されたときに、.headerの見た目を変更したいので、.headerをstickyな.containerで囲んでいます。

.container {
  position: sticky;
  top: 0;
  container-type: scroll-state;
}

.header {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 10px;
  font-size: 20px;
}

.header ul {
  display: flex;
  gap: 20px;
  align-items: center;
  list-style: none;
}

@container scroll-state(stuck: top) {
  .header {
    background-color: rgba(255, 255, 255, 0.7);
    backdrop-filter: blur(5px);
    box-shadow: 0 5px 10px #ccc;
    border-radius: 10px;
  }
}

stickyにした.containerが固定されたときの見た目を変更したいので、.containercontainer-type: scroll-state;を指定し、スクロールの状態を検知するクエリーコンテナにします。

そして見た目を変更したい子要素の.headerには、コンテナクエリーscroll-state(stuck: top)を指定した@containerブロックを加え、上部に固定されたときの見た目を記述します。

デモでは少しスクロールすると、.containertop: 0;の位置に来るため、見た目が変わるようになりました。

条件stuckで使える検知方法は次のとおりです。

  • none
  • top
  • right
  • bottom
  • left
  • block-start
  • inline-start
  • block-end
  • inline-end

要素が軸にスナップされたときに何かする

スクロールできる要素をユーザーがスクロールしたとき、通常はユーザーが操作したスクロール量に応じた距離だけ移動します。CSS Scroll Snapを使うと、ユーザーのスクロール操作に応じて、任意の要素にスナップできます。

CSS Scroll Snapについては今日から使えるCSS | 第5回 CSS Scroll Snapがありますが、その中のデモを一度見てみましょう。

このデモではおよそ画面の半分ほどのスクロール操作を行うと次の要素へスナップし、それ未満の場合はもとの位置に戻るようになっています。

<body class="ScrollSnap">
  <div class="ScrollSnap-item">1</div>
  <div class="ScrollSnap-item">2</div>
  <div class="ScrollSnap-item">3</div>
  <div class="ScrollSnap-item">4</div>
  <div class="ScrollSnap-item">5</div>
</body>

CSSは次のようになっています。scroll-snap-type: y mandatory;とすることで、y方向(縦方向)にスナップするようになっています。

html, body {
  width: 100%;
  height: 100%;
}
html {
  overflow: hidden;
}
body {
  margin: 0;
  overflow: auto;
}
.ScrollSnap {
  scroll-snap-type: y mandatory;
}
.ScrollSnap-item {
  scroll-snap-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  font-size: 50vh;
  text-shadow: 2px 2px 0 rgba(255, 255, 255, 0.3);
}
.ScrollSnap-item:nth-child(1) {
  background-color: floralwhite;
}
/* 以下要素ごとに色を変えるコード。省略 */

このデモに、コンテナクエリーを使い、スクロールがスナップしたときに、子要素の見た目を変えるCSSを追加します。スナップされたときに文字がズームアウトしてくるような動きを加えます。

HTMLは次のようになっています。スナップするのは.ScrollSnap-itemで、スナップしたときにその子要素のスタイルを変えたいので、spanで囲っておきます。

<body class="ScrollSnap">
  <div class="ScrollSnap-item"><span>1</span></div>
  <div class="ScrollSnap-item"><span>2</span></div>
  <div class="ScrollSnap-item"><span>3</span></div>
  <div class="ScrollSnap-item"><span>4</span></div>
  <div class="ScrollSnap-item"><span>5</span></div>
</body>

CSSではスナップする要素にcontainer-type: scroll-state;を追記します。次に、スナップする要素の子要素のスタイルを定義します。/* 追記2 */の部分がスナップしていないときのスタイルで、/* 追記3 */がスナップしたときのスタイルとなっています。

.ScrollSnap-item {
  scroll-snap-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  font-size: 50vh;
  text-shadow: 2px 2px 0 rgba(255, 255, 255, 0.3);
  container-type: scroll-state; /* 追記1 */
}

/* 追記2 */
.ScrollSnap-item > span {
  scale: 1.2;
  opacity: 0;
  transition: all 2s 0.5s; /* 0.5s遅らせて2sかけて変化する */
}

/* 追記3 */
@container scroll-state(snapped: y) {
  .ScrollSnap-item > span {
    opacity: 1;
    scale: 1;
  }
}

このデモは縦方向にスクロールしスナップするようになっているので、snapped: yとしています。横にスクロールしてスナップするという場合はsnapped: xとします。

条件snappedで使える検知方法は次のとおりです。

  • none
  • x
  • y
  • block
  • inline

要素がスクロールできるときになにかする

条件scrollableでは、「要素が上や下方向にスクロールできるとき」という判定ができます。それを使って、スクロール時に現れるページトップに戻るリンクが作れます。

まずはデモを見てみましょう。

デモを少しだけスクロールすると、右下に「ページトップへ」リンクが表示されます。それをクリックするとスルスルとページトップにスクロールし、「ページトップへ」リンクが非表示になります。

HTMLは次のようになっています。特筆するところはありません。

<h1>スクロールすると「ページトップへ」が現れる</h1>

<p>
  The quick brown fox jumps over the lazy dog.
  <!-- 省略 -->
</p>

<a class="goToPageTop" href="#top">ページトップへ</a>

次にCSSです。

まず、スクロールが発生するhtmlcontainer-type: scroll-state;を指定してクエリーコンテナにしています。

html {
  container-type: scroll-state;
  scroll-behavior: smooth;
}

次にページトップへのリンクである.goToPageTopです。画面右下に配置しています。さらにtranslate: 0 100%;を指定して、要素を画面の下へ押し下げ、見えない位置に隠しておきます。

.goToPageTop {
  position: fixed;
  bottom: 0;
  right: 0;
  background-color: #000;
  color: #fff;
  padding: 10px;
  border-radius: 10px 10px 0 0;
  transition: translate 0.2s;
  translate: 0 100%;
}

続いてコンテナクエリーの指定部分です。「ページトップへ」が現れる条件は、上方向にスクロールが可能であるときなので、scrollable: topを使います。条件にマッチしたときに、隠れていた「ページトップへ」が出てきます。

@container scroll-state(scrollable: top) {
  .goToPageTop {
    translate: 0 0;
  }
}

これまでスクロールしたらページトップへを出すという実装にはJavaScriptを使っていましたが、CSSのみでできるようになりました。

JavaScriptを使う場合は、「100px以上スクロールしたら」というような条件をつけることができるので、どちらか一方が優れているというわけではありません。要件次第ではJavaScriptを使った実装が必要になるときもあります。

条件scrollableで使える検知方法は次のとおりです。

  • none
  • top
  • right
  • bottom
  • left
  • block-start
  • inline-start
  • block-end
  • inline-end
  • x
  • y
  • block
  • inline

ここまでのまとめ

scroll-state() はスクロールに関する状態をCSSだけで判定できる新しいコンテナクエリーです。判定できる状態は「端に固定されたとき(stuck)」「軸にスナップされたとき(snapped)」「スクロール可能なとき(scrollable)」の3種類で、それぞれに応じて子要素の見た目を切り替えられます。

これまではJavaScriptを用いてスクロール位置を監視しなければならなかった処理も、CSSだけで記述できるようになりました。stickyヘッダーの装飾、スナップ中の要素の強調、スクロールに応じたリンク表示といった典型的なパターンが、より簡単に実装できるようになります。

次回は、ここで紹介した基本を踏まえ、実際のサイトで活用できるより実践的なアイデアを取り上げます。