CSSのコンテナクエリで position: fixed の扱い方が変わった件

「最強の剣なのに呪われている」みたいなコンテナクエリの呪縛が、いつの間にか解けていたのでシェアしたいと思います。

コンテナクエリの呪縛

2023年にコンテナクエリに関する記事を書きました。この時には、container-type プロパティが設定された要素 (コンテナ要素) の子孫要素に position: fixed を設定しても position: absolute のように配置されていました。

MDN Web Docs の「レイアウトと包含ブロック」の「包含ブロックの識別」には以下のような記述があります。

position プロパティが absolute または fixed の場合、包含ブロックは以下の条件を持った直近の祖先要素におけるパディングボックスの辺によって構成されることがあります。

  • filterbackdrop-filtertransformperspective の値が none 以外である。
  • contain の値が layoutpaintstrictcontent のいずれかである。(例 contain: paint;
  • container-type の値が normal 以外である。
  • will-change 値で、包含ブロックを形成する初期値以外の値を持つプロパティ(filtertransform など)がある。
  • content-visibility の値が auto である。
https://developer.mozilla.org/ja/docs/Web/CSS/CSS_display/Containing_block

例えば、body要素をコンテナ要素にした場合、position: fixed でスクロールに追随するはずのヘッダーやハンバーガーメニューが追随しなくなるといったことが発生します。MDN Web Docs によれば、これが仕様どおりの挙動のようでした。

…
<style>
body {
  container: body / inline-size;
}
header {
  position: fixed; /* absolute のように振舞う */
    inset-block-start: 0;
    inset-inline: 0;
  z-index: calc(infinity);
}
</style>
…
<body>
  …
  <header> … </header>
  …
</body>
…

スクロールに追随するヘッダーとコンテナクエリを組み合わせたいだけなら、ヘッダーをコンテナ要素外に配置すればいいのですが、background-attachment: fixed がいまだに使えない Safari のための position: fixed を使用したハックなどもコンテナ要素内では機能しなくなるため、コンテナクエリが使えずにいました。

…
<style>
header {
  position: fixed; /* コンテナ要素外なので機能する */
    inset-block-start: 0;
    inset-inline: 0;
  z-index: calc(infinity);
}
.container {
  & {
    container: container / inline-size;
  }
  figure {
    & {
      block-size: 100dvb;
      clip-path: inset(0);
    }
    img {
      block-size: 100%;
      inline-size: 100%;
      object-fit: cover;
      position: fixed; /* コンテナ要素内なので機能しない */
        inset: 0;
      z-index: -1;
    }
  }
}
</style>
…
<body>
  …
  <header> … </header>
  <div class="container">
    <figure><img … /></figure>
  </div>
  …
</body>
…

仕様やブラウザの挙動がいつ頃から変わったのかは分かりませんが、現在ではコンテナ要素内で position: fixed が普通に使えるようになっています。

コンテナクエリでできることのおさらい

@container による分岐や cqw, cqh, cqi, cqb, cqmin, cqmax といったコンテナ要素を基準とした単位が使用できるようになります。

コンテナ幅によるレイアウト設定

コンテナ要素の幅を基準にしたレイアウトが設定できます。

body {
  & {
    container: body / inline-size;
    margin: 0;
  }
  @container body (960px >= width) {
    /* body要素の幅が 960px 以下なら */
  }
  @container body (width > 960px) {
    /* body要素の幅が 960px より大きいなら */
  }
}
.flex {
  & {
    container: flex / inline-size;
    display: flex;
    flex-flow: wrap;
  }
  & > * {
    flex-basis: clamp(20%, (40rem + 1px - 100cqi) * 1e5, 100%);
    /* .flex のインラインサイズが 40rem より大きいなら 20% それ以下なら 100% */
  }
}

スクロールバーを含まないルート要素の幅

ルート要素に近いbody要素などをコンテナ要素にすることで、100vw, 100vi からスクロールバーを除いた幅が 100cqw, 100cqi で取得でき、画面いっぱいに表示するような表現が手軽に実現できるようになります。

…
<style>
body {
  container: body / inline-size;
  margin: 0;
}
.wrapper {
  inline-size: 640px;
  margin-inline: auto;
}
.alignfull {
  margin-inline: calc(50% - 50cqi);
  /*
  body要素の幅が 1280px だった場合、
  50% (320px) - 50cqi (640px) = -320px
  となり、要素がbody要素いっぱい (320px + 640px + 320px = 1280px) に広がる。
  */
}
.alignwide {
  margin-inline: calc(50% - min(960px / 2, 50cqi)); /* 960px の部分は変更可能 */
  /*
  body要素の幅が 1280px だった場合、
  50% (320px) - 960px / 2 = -160px
  50% (320px) - 50cqi (640px) = -320px
  となり、960px / 2 が 50cqi より小さいので要素は 960px (160px + 640px + 160px) まで広がる。
  body要素の幅が 720px だった場合、
  50% (320px) - 50cqi (360px) = -40px
  となり、960px / 2 より 50cqi が小さいので要素はbody要素いっぱい (40px + 640px + 40px = 720px) に広がる。
  */
}
</style>
…
<body>
  <div class="wrapper">
    <figure class="alignfull"><img … /></figure>
    <figure class="alignwide"><img … /></figure>
  </div>
</body>
…

コンテナクエリの呪縛が解けたので、積極的に使っていきたいですね。ただ、いつから現在のようになったのかは分からずじまいなので、知っている方はコメントなどで教えてください。

関連記事