スマートフォン・タブレットからインターネットサーバーオペレーション

APPW.jp
 
CSS NOTES ②

クリップとスクロールの境界
hidden / clip / overscroll-behavior

overflow:hiddenoverflow:clip は、見た目の結果が同じです。 違うのはスクロール文脈を作るかどうか。 この一点が position:sticky を壊したり、装飾を消したりします。 「隠す」系の指定を、境界線で整理します。

overflow clip sticky overscroll-behavior

この記事で扱うこと
  1. hidden は、スクロールできる箱である 見えないだけで、スクロール文脈は生まれている
  2. clip との違いは、そこだけ 7項目の対比表
  3. sticky が効かなくなる理由 基準になる祖先が入れ替わる
  4. overscroll-behavior でラバーバンドを止める -x に限る理由
  5. border-radius と、消える装飾 クリップの基準はパディングボックス
  6. body の overflow は、ビューポートへ伝播する html に書くべき場面
  7. まとめの早見表 どちらを選ぶか
[ 1 ]

hidden は、スクロールできる箱である

overflow:hidden は「はみ出しを隠す」指定として使われます。 間違いではありませんが、正確にはこうです。

仕様。 overflow:hiddenスクロール可能な箱を作り、 そのスクロールバーを表示しない——それだけです。 スクロール文脈(scroll container)は生まれています

証拠は簡単に確かめられます。

hidden でも動く
// overflow:hidden の要素でも、JSからは動かせる
box.scrollLeft = 200;      // 動く
box.scrollTop  = 100;      // 動く

// キーボード操作やアンカー移動でも、勝手にずれることがある

つまり hidden「隠す」というより「スクロールできるが、操作手段を見せない」状態です。 この性質が、いくつかの副作用の源になります。

[ 2 ]

clip との違いは、そこだけ

overflow:clip は、スクロール文脈を作らずにクリップだけする指定です。 比べると、違いは1点に集約されます。

項目hiddenclip
はみ出しをクリップするするする
スクロールバーを出す出さない出さない
スクロール文脈を作る作る作らない
JS で scrollLeft が動く動く動かない
子孫の position:sticky効かなくなる場合影響しない
スクロール位置の復元対象なるならない
使い分けの原則。 スクロールさせる意図が無いなら clip。 「はみ出しを切りたいだけ」なら、余計な文脈を作らないほうが安全です。 逆に JS でスクロール位置を制御したいなら hidden が要ります。

なお clip は比較的新しい値です。対応していない環境では 宣言そのものが無視され、visible のままになります。 クリップされないと困る場面では、フォールバックを考えてください。

[ 3 ]

sticky が効かなくなる理由

position:sticky の要素は、最も近いスクロール文脈を基準に張り付きます。 通常はビューポートです。

祖先に hidden があると
.outer { overflow: hidden; }   ← スクロール文脈になる

.outer .bar {
  position: sticky;
  top: 0;                   ← 基準が .outer に変わる
}

基準が .outer に移ると、.outer の中でのスクロール量を見ることになります。 ところが .outer 自身はスクロールしません(中身が収まっている、あるいは外側がスクロールしている)。 動く余地がゼロなので、張り付く動作も起きません

要素は消えず、エラーも出ません。ただスクロールに追従しなくなるだけです。 原因が sticky 自身ではなく祖先にあるため、見つけにくい部類の不具合です。

clip なら影響しない
.outer { overflow: clip; }     ← 文脈を作らない

.outer .bar {
  position: sticky;
  top: 0;                   ← 基準はビューポートのまま
}
実際の判断
自分のサイトでは body に横方向のクリップを入れる必要がありました。 ターミナル風のバーを position:sticky にしている記事があったため、 hidden ではなく clip を選びました。 実機で確認して、sticky が保たれることも確かめています。
[ 4 ]

overscroll-behavior でラバーバンドを止める

iOS Safari では、縦にスクロールしながら指を左右に振るとページ全体がずれ、離すと戻ります。 いわゆるラバーバンド(オーバースクロール)です。

overflow-x:hidden を入れてもこれは残ります。 スクロールを止めることと、端での弾性を止めることは別だからです。

スクロールの連鎖端での弾性
auto(既定)親へ連鎖する起きる
contain止める起きる
none止める止める
横だけを止める
html {
  overscroll-behavior-x: none;
}
-x に限る理由。 overscroll-behavior: none と全方向に指定すると、 縦の「引っ張って更新」も止まります。 モバイルの標準操作を奪うことになるので、 止めたい方向だけを指定するのが安全です。
実際に効いた場面
iPad mini の Safari で、縦スクロール中に指を左右に振るとページがずれる現象がありました。 overflow-x:hidden では止まらず、 overscroll-behavior-x:none を入れて解消しています。 これが効いたということは、はみ出しは無く純粋な弾性だったという裏付けにもなりました。
[ 5 ]

border-radius と、消える装飾

角丸のカードに、枠の四隅へ装飾を置こうとしたときの話です。

表示されない装飾
.card {
  position: relative;
  border: 1px solid #333;
  border-radius: 10px;
  overflow: hidden;
}
.card::before {
  content: "";
  position: absolute;
  top: -1px; left: -1px;   ← 枠に重ねたい
  width: 13px; height: 13px;
  border-width: 1px 0 0 1px;  ← L字のかぎ括弧
}
クリップの基準はパディングボックス。 overflow によるクリップはボーダーの内側で行われます。 -1px に置いた1pxの線は、まるごと範囲外。 さらに border-radius の丸みに沿って角が削られるため、 この装飾は一度も表示されません

回避策は3つあります。

方法代償
装飾を内側(top:3px など)に置く枠に重ねる意匠は諦める
overflow を外す中身のはみ出しが切れなくなる
装飾を親要素側へ移す構造が一段増える
この項目について
クリップ基準の説明は仕様に基づくもので、 ブラウザごとの差異までは実測していません。 「装飾が表示されない」という結果は自分のページで確認していますが、 その原因が丸みによるものか -1px によるものかは、切り分けていません。
[ 6 ]

body の overflow は、ビューポートへ伝播する

ページ全体の横スクロールを止めたいとき、body に書くか html に書くかで迷います。 仕様上の扱いはこうです。

伝播の規則。 ビューポートの overflow は、まず html から取られます。 htmlvisible のときに限り、body の値が代わりに使われます。 伝播した側の要素は、自身の使用値が visible になります。

つまり body{overflow-x:hidden} だけでも、 html に何も指定が無ければビューポートに効きます。 逆に html 側に何か指定があると、body の値は伝播せず body という箱の中の話になります

迷わない書き方
/* ビューポートを止めたいなら html に書く */
html { overflow-x: hidden; }

/* body 自身の箱をクリップしたいなら body に書く */
body { overflow-x: clip; }
この項目について
伝播の規則は仕様の記述に基づきます。 自分のページでは html{scroll-behavior:smooth} を先に書いていたため、 body だけでは足りず html にも書く必要がありましたが、 それが伝播の停止によるものか別の要因かは切り分けていません。 迷う場面では、意図する側に明示的に書くのが確実です。
[ デモ ]

hidden と clip、どちらを選ぶか

場面を選ぶと、両者の結果を並べて表示します。 同じになる場面と、決定的に違う場面があります。

Interactive · hidden vs clip
場面ごとの選択
仕様に基づく挙動の対比です。実際の描画は行っていません。
overflow: hidden
overflow: clip

差が出るのはスクロール文脈が関わる場面だけです。 単にはみ出しを切りたいなら、どちらでも結果は同じ。 だからこそ、余計な文脈を作らない clip を既定にしておくのが素直だと思います。

[ 7 ]

まとめの早見表

やりたいこと書くもの
はみ出しを切るだけoverflow: clip
JS でスクロール位置を制御したいoverflow: hidden
子孫に sticky があるclip(hidden は避ける)
ページの横スクロールを止めるhtml { overflow-x: hidden }
iOS の左右の揺れを止めるoverscroll-behavior-x: none
引っ張って更新は残したいnone を全方向に書かない
角丸の枠に装飾を重ねたいoverflow を外すか、内側に置く

この記事のまとめ

  • hiddenスクロール可能な箱を作り、バーを見せないだけ。文脈は生まれている。
  • clip文脈を作らずクリップする。違いはそこだけ。
  • 祖先の hidden子孫の sticky の基準を奪う。動く余地が無く、張り付かなくなる。
  • ラバーバンドは overflow では止まらない。overscroll-behavior-x: none
  • 全方向に none を書くと引っ張って更新も止まる。方向を限る。
  • クリップの基準はパディングボックス。枠外に置いた装飾は消える。
  • ビューポートの overflowhtml から取られ、htmlvisible のときだけ body が使われる。
CSS NOTES シリーズ
  1. 縮まないと、あふれる
    min-width:0 と overflow の関係
  2. クリップとスクロールの境界(この記事)
    hidden / clip / overscroll-behavior
  3. 背景と合成レイヤー
    ::before / fixed / will-change

『クリップとスクロールの境界 — hidden / clip / overscroll-behavior』を公開しました。