CSSレイアウト計測の罠: zoom、transform、min-width、inline style
レイアウト崩れは、見えているCSSルールそのものではなく、ブラウザがどう寸法を測るか、grid/flex itemがどこまで縮むか、inline styleがmedia queryに勝つかで起きることがあります。
この記事では、レスポンシブUIで何度も問題になる4つの罠を扱います。
zoomとtransform: scale()はlayoutへの影響が違うoffsetHeightとgetBoundingClientRect()は別の質問に答える- grid/flex itemの中に広いcontentがある時は
min-width: 0が必要 style={{ display: "flex" }}はmedia queryの非表示指定に勝つ
zoomとtransformは役割が違う
zoom は要素を拡大縮小し、layout flowにも影響します。transform: scale() は見た目を変えますが、兄弟要素のために確保されるlayout spaceは変えません。
JavaScriptでcontrolの高さを測り、別要素の位置決めに使う場合、この違いが効きます。
const height = element.offsetHeight;
offsetHeight はlayout boxの値です。render後の見た目の高さとはズレることがあります。
const height = element.getBoundingClientRect().height;
getBoundingClientRect() はrendered geometryの値です。overlay、sticky control、terminal pane、mobile dock、animated barでは、こちらが必要なことが多いです。
表示位置にはgetBoundingClientRectを使う
下部control barの高さを親layoutへ伝えるなら、見た目の高さを返します。
const observer = new ResizeObserver(() => {
const height = controlRef.current?.getBoundingClientRect().height ?? 0;
onHeightChange(height);
});
if (controlRef.current) {
observer.observe(controlRef.current);
}
これは次の要素で特に重要です。
zoom- CSS transform
- 見た目のsizeが変わるtransition
- mobile viewport変化
- browserのdesktop site表示
offsetHeight に推測したzoom倍率を掛けるより、render後の結果を直接測ります。
広い子要素を持つgrid/flex itemにはmin-width: 0を入れる
1fr のgridでも、1つのcode blockで横overflowすることがあります。
原因はgrid/flex itemのdefault min-width: auto です。itemが子要素のintrinsic widthより小さく縮まないためです。
.article-layout {
display: grid;
grid-template-columns: 1fr;
}
.article-layout > * {
min-width: 0;
}
pre {
overflow-x: auto;
}
flexでも同じです。
.row {
display: flex;
}
.row > * {
min-width: 0;
}
対象になりやすい子要素です。
<pre>code block- table
- 長いURL
- spaceなしの長いtoken
- chartやSVG
目的はoverflowを全体で隠すことではありません。親trackを縮め、広い子要素だけを自分のbox内でscrollさせることです。
inline styleはmedia queryを負かす
React inline styleは優先度が高いです。
<div className="desktop-toolbar" style={{ display: "flex" }}>
...
</div>
このmedia queryでは隠れません。
@media (max-width: 767px) {
.desktop-toolbar {
display: none;
}
}
inlineの display: flex が勝つためです。display指定はclassへ移します。
<div className="desktop-toolbar">...</div>
.desktop-toolbar {
display: flex;
}
@media (max-width: 767px) {
.desktop-toolbar {
display: none;
}
}
!important で戦うより、layout propertyの所有場所をclass側に寄せるほうが保守しやすいです。
mobile wake時のvisualViewport値を検証する
mobile browserは、device wakeやpage restore直後に変な visualViewport.height を返すことがあります。
visualViewport を使うなら、物理的にありえない値を捨てます。
function isValidViewportHeight(vvHeight: number, innerHeight: number) {
return vvHeight >= innerHeight * 0.35;
}
function updateFromViewport() {
const viewport = window.visualViewport;
if (!viewport) return;
if (!isValidViewportHeight(viewport.height, window.innerHeight)) {
return;
}
updateLayout(viewport.height);
}
wakeやrestoreでは、一度の requestAnimationFrame より短いtimeoutのほうが安定する場合があります。
デバッグチェック
レスポンシブlayoutがclip/overflowする時は、順に確認します。
offsetHeightとgetBoundingClientRect()を比べる- 測っている要素に
zoomやtransformがあるか見る - 広いcontentを持つgrid/flex itemに
min-width: 0を入れる - media queryで制御したいpropertyにinline
styleがないか探す - placeholderではなく実データで狭いviewportを試す