はじめに
この記事は、Liquid Glass の Safari で Web ページを開いたときに、タブバーの裏にもコンテンツを表示する方法を紹介するものです。
どこがどうなってるか分かりづらいかもしれませんが、こんな変化があります。

これだけです。
お分かりいただけたでしょうか。
タブバーの裏にも背景を描画できるようにしています。
ちなみにこちらの背景に使用している素敵な動画は
Kanenoriさんという方が Pixabay にアップロードされていたものです。
他にも素敵な動画や画像があったのでもしよければぜひ覗いてみてください!
話がそれましたが、本記事ではこれをどのように実現しているのかを説明します。
結論
コンテンツの上下にバー1枚分の余白を足し、その余白分だけスクロールした位置に居座らせ続けます。そうすると、ページが常にスクロール途中の状態に保たれるので、バーの裏までコンテンツを描けるようになります。
もう少し補足すると、上端や下端に接するとバー裏はキャンバスの色で塗りつぶされます。それを起こさせないことを目的とした実装です。
この記事を書こうと思ったきっかけ
あまり記事がなかったからです(レアケース?)。
まず前提として、この「バー裏にもコンテンツを表示させておきたい」という要求は業務上で発生したものです。
私は模様ありの背景(四葉のクローバーが並んでるイメージ)が動くコンテンツを作成していたのですが、要求があったページは1カットで収まるスクロールの発生しないページでした。
なので、上端と下端で背景のクローバー模様がバッサリと切れていて見栄えが少し悪かったのだと思います。
単色の背景や、グラデの背景などであればあまり気にならないかもしれませんが、模様付きの背景であったため上下の見切れが顕著に感じられた、といったところでしょうか。
自分が言われた時は「まあそうか」程度の温度感で、「できそうだよね?」と打診された時は「なんかそれ用のプロパティとかありそうだし Claude もあるしそんなに時間かからないかな〜」と思っていました。
ただ、実装してみると意外と時間がかかったので、この度誰かの助けになればいいなと思い記事にしてみた次第です。
綺麗な方法ではないのでもっといい方法があるんじゃないかとも思っています。有識者の方がいればぜひ教えていただけるとありがたいです。
仕組み
結論にも書いた通りですが、仕組みとしては何か特別なことをしているわけではなくて、常にコンテンツの途中状態で留まるようにしています。Safari では上端にも下端にも接していなければバー裏にもコンテンツを表示するので、その状態を利用している感じですね。
やることは3つだけです。
1. スクロールできる領域を用意する
バー1枚分の余白を、以下では edge と呼びます。スクロールできる領域を、画面より edge × 2 だけ高くします。
.scroller {
height: calc(100svh + var(--edge) * 2);
overflow-y: auto;
}
2. 中身に同じ幅の余白を持たせる
.page {
padding-block: var(--edge);
}
3. edge の分だけスクロールした位置へ移動する
window.scrollTo({ top: edge, behavior: 'instant' })
見た目の位置がずれないのは、手順2の padding のおかげです。文書を edge 分上へ動かした一方で、中身は edge 分下から始まっています。
差し引きゼロなので、コンテンツの見え方は普通のページと同じになります。
ただし、一度置いて終わりにしてしまうと、ステータスバーのタップ、回転、キーボードの開閉、戻る操作での復帰など、ブラウザ側で文書を動かすイベントに影響されてしまいます。
なのでそれらのイベントを拾って、同じ位置へ引き戻し続ける必要が出てきます。
これで、タブバーの裏にコンテンツを表示する土台はできました。
タブバーの裏に何を出すか
パターンA:ページ全体で共通の背景を設定したい場合
ページ全体で共通の背景を出したいなら、上下の帯まで届く1枚のレイヤーが必要です。
これが背景レイヤーです。
ここには、position: sticky を使います。sticky はビューポートではなくスクロール領域に対して張り付くので、どのスクロール位置でも両方の帯を覆い続けます。
.background {
position: sticky;
top: 0;
z-index: 0;
height: calc(100svh + var(--edge) * 2);
/* 高さの分だけ後続を押し下げないよう、同じ量を打ち消す */
margin-bottom: calc(-1 * (100svh + var(--edge) * 2));
}
置く場所はスクロール領域の直下、コンテンツ層より先です。単色でもグラデーションでも画像でも動画でも同じです。
<div class="scroller">
<div class="background"></div>
<div class="page">
<!-- 中身 -->
</div>
</div>
コンテンツ層を背景レイヤーの上に出したいので、並び順をこのようにしています。
パターンB:セクションごとに背景を設定したい場合
セクションごとに背景が変わるページはパターンAとは逆で、当然ですが各セクションに背景を持たせます。
ただし、先頭と末尾のセクションだけは例外です。この2つは余白の内側にいるので帯の位置には何もありません。
解決策は、背景だけを edge 分外へ広げ、中身は元の位置のままにすることです。
これを透明な枠線で実現します。
太さ edge の枠線を上辺に1本付け、透明にするとそこに背景が見えます。
.page > :first-child {
border-block-start: var(--edge) solid transparent;
margin-block-start: calc(-1 * var(--edge));
background-origin: border-box;
}
.page > :last-child {
border-block-end: var(--edge) solid transparent;
margin-block-end: calc(-1 * var(--edge));
background-origin: border-box;
}
枠線で背景だけを edge 分外へ広げ、同じ量の負のマージンで中身を元の位置に戻しています。はみ出すのは枠線の帯だけです。
background-origin は背景画像のために必要です。この1行で基準を枠線の外側に揃えます。
padding で同じこともできると思いますが、border を選んだのは、個別の実装者がそのセクションに書いた padding と衝突させないためです。
その他:全画面のオーバーレイを使う場合
モーダルやナビゲーションには position: fixed が使えません。固定要素はバーの裏に描画されないので、バーの部分だけ切れます。背景レイヤーと同じ作りで、z-index を上げたものを使います。
.overlay {
position: sticky;
top: 0;
z-index: 10;
height: calc(100svh + var(--edge) * 2);
margin-bottom: calc(-1 * (100svh + var(--edge) * 2));
}
オーバーレイ表示中に背後のスクロールを止めるには、touch-action: none を使ってください。
sticky なので、これもスクロール領域の直下、コンテンツ層より先に置きます。コンテンツ層の内側だと上の帯まで届きません。手前に出すのは z-index の側です。
<div class="scroller">
<div class="background"></div>
<div class="overlay"></div>
<div class="page">…</div>
</div>
ポイント
塗る場所は1つに絞る
ドキュメントの中身は全部バー裏に届きますが、それとは別にキャンバス(html / body)の色も薄い膜としてバーに乗ります。両方に色を置くと、どちらでもない色のバーができあがります。
- ページ全体の背景は背景レイヤーに置く
- コンテンツ層は塗らない(背景レイヤーを完全に覆ってしまう)
- html と body は塗らない。フォールバックのつもりの色は、全部のバーに残る恒久的な色になる
- セクション背景だけは例外。ページ全体ではないので、セクション側に置く
iOS Safari 以外では普通のページに戻す
ここまでのコードは、どの環境でも動いてしまいます。バーが半透明でないブラウザでは、余白と引き戻しは何の役にも立たないのに overflow: hidden だけが残る、という状態になります。なので、仕組み全体に適用条件を付けます。
条件はスタイルシートの1箇所だけに書きます。iOS Safari に絞るには、タッチ操作であることと WebKit であることの2つを見ます。
:root { --edge: 0px; }
@media (hover: none) and (pointer: coarse) {
@supports (-webkit-touch-callout: none) {
:root { --edge: 140px; }
html, body { overflow: hidden; }
}
}
条件から外れた環境では --edge が 0px のままなので、スクロール領域の高さは 100svh に、コンテンツ層の余白は 0 になります。何もしていないページと同じです。
JS 側は条件を書き直さず、CSS が出した答えを読み返します。
const edge = parseFloat(
getComputedStyle(document.documentElement).getPropertyValue('--edge'),
)
if (edge > 0) pin()
同じ条件を CSS と JS の両方に書くと、片方だけ真になったときに「中間位置に居座ったまま普通のページ」という壊れ方をします。バーの裏には何も無いのに、上下に空白が空いたページだけが残る、という最悪の状態です。判定を1箇所に寄せておけば、この事故は起きません。
制約
大きいものは2つです。
1つは、window.scrollY を自分で動かせないことです。ページ内アンカー、scrollIntoView、フォーカス移動はどれも文書を動かすので、引き戻しに打ち消されます。要素まで移動したいときは、window ではなくスクロール領域の座標で計算します。
const box = target.getBoundingClientRect()
const frame = scroller.getBoundingClientRect()
scroller.scrollTo({ top: box.top - frame.top + scroller.scrollTop - edge })
もう1つは、祖先に overflow: hidden を付けられないことです。sticky なレイヤーが動かなくなるので、背景レイヤーもオーバーレイも消えます。
このほかにも、組み込むと細かい制約が出てきます(scroll-behavior: smooth との相性、透明ボーダーを持つ要素の高さ計算、スムーススクロール系ライブラリの向き先など)。
一番ハマったところ
iPhone側の設定です。
iPhoneの設定→アクセシビリティ→画面表示とテキストサイズ→透明度を下げるがONになってると背景が透過しなくなります(他にも要因があるかもしれません)。
多分Liquid Glassを無効にするための設定なんでしょうかね。
iPhone機種の違い、OSのバージョンなどを比べながら調べていましたが、一つのiPhoneだけこの仕組みが動かなくて「全然わからん」と悩んでいました。
幸いにも、iPhoneの所有者がその設定を自らしていたことを覚えていたので、原因が解明されたといった次第です。
トレードオフ
得られるのは、iOS Safari で端まで使えるビジュアルだけです。
いざ実装してみると見た目は結構いいのですが、代わりに手放すものもあります。
- 縦向きでのアドレスバーの畳み込み
- ページ全体が従う必要のある文書構造
などがあります。
加えて、将来の iOS で変わりうる Safari の非公開な合成挙動に依存することになります。
ライブラリにもしました
この記事の仕組みをそのまま npm パッケージにしてあります。
npm i under-liquid-glass
- npm: https://www.npmjs.com/package/under-liquid-glass
- GitHub: https://github.com/ocean6954/under-liquid-glass
導入手順とオプション、それとこれをライブラリとして配ろうと思った理由は、別の記事に書きました。
https://qiita.com/ocean6954/items/3e826fbb5b26aedbc887
おわりに
この仕組みを作るのに4時間くらいかかりました。Claudeを使用した上でこのくらいです。
なんなら最初にClaudeにも「無理だ」と言われたのでこのAI時代の実装でもちょっと苦労しました。
先人の知恵みたいなのもほとんどなかったので、需要がないか、そもそも悪手なのでみんなやってないのか、はたまた自分が先駆者になっていたのか。
どれかはわかりませんが、もし世の中に同じ要求で困ってる人がいて、この記事が何かの役に立てれば嬉しいな〜と思っています。
読んでいただきありがとうございました。







