
CSSのコンテナクエリで position: fixed の扱い方が変わった件
「最強の剣なのに呪われている」みたいなコンテナクエリの呪縛が、いつの間にか解けていたのでシェアしたいと思います。
コンテナクエリの呪縛
2023年にコンテナクエリに関する記事を書きました。この時には、container-type
プロパティが設定された要素 (コンテナ要素) の子孫要素に position: fixed
を設定しても position: absolute
のように配置されていました。
MDN Web Docs の「レイアウトと包含ブロック」の「包含ブロックの識別」には以下のような記述があります。
position
プロパティがabsolute
またはfixed
の場合、包含ブロックは以下の条件を持った直近の祖先要素におけるパディングボックスの辺によって構成されることがあります。
filter
、backdrop-filter
、transform
、perspective
の値がnone
以外である。contain
の値がlayout
、paint
、strict
、content
のいずれかである。(例contain: paint;
)container-type
の値がnormal
以外である。will-change
値で、包含ブロックを形成する初期値以外の値を持つプロパティ(filter
やtransform
など)がある。content-visibility
の値がauto
である。
例えば、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>
…
〆
コンテナクエリの呪縛が解けたので、積極的に使っていきたいですね。ただ、いつから現在のようになったのかは分からずじまいなので、知っている方はコメントなどで教えてください。