行動瀏覽器有兩個視口。**佈局視口(layout viewport)**是 CSS 版面計算的基準,vh/vw、position: fixed、初始包含塊都掛在它上面。視覺視口(visual viewport)是使用者此刻真正看到的那塊,會隨捏合縮放與工具列展開收合而變。100vh 之所以在手機上「爆掉」,就是因為它綁的是佈局視口,而佈局視口的高度取自工具列收起時的狀態——工具列展開時,你的 100vh 元素底部有一截被壓在工具列底下。
這不是 bug,是規範選擇:如果 vh 跟著工具列變,任何用 vh 排版的頁面在捲動時都會不斷重排。付出的代價就是滿版元素會超出可視區。
meta viewport:完整寫法與每個欄位的意義
<meta name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">| 欄位 | 值 | 對滿版遊戲的意義 |
|---|---|---|
width=device-width | 固定 | 讓佈局視口等於裝置寬度。不寫的話行動瀏覽器會假設 980px 寬再縮小整頁 |
initial-scale=1 | 固定 | 初始不縮放。與 width=device-width 同時寫是為了繞過舊 iOS 的轉向縮放 bug |
viewport-fit | auto(預設)/contain/cover | 必須 cover 才能畫到瀏海與圓角區域;也只有 cover 才會讓 env(safe-area-inset-*) 變成非零 |
interactive-widget | resizes-visual(預設)/resizes-content/overlays-content | 決定虛擬鍵盤彈出時哪個視口縮小。Chrome 108+、Firefox 132+ 支援,Safari 尚未 |
user-scalable / maximum-scale | no / 1 | 有無障礙代價,見下 |
user-scalable=no 的取捨
MDN 的立場明確:關閉縮放會讓低視力使用者無法閱讀內容,違反 WCAG 2.1(要求至少可放大到 200%)。而且 iOS Safari 從 10 起就忽略 user-scalable=no,寫了也擋不住捏合縮放。
滿版遊戲確實需要擋掉雙擊縮放(否則快速連點會觸發放大),但正確工具是 CSS 而不是 meta:
.game-root {
touch-action: none; /* 完全接管手勢:不捲動、不縮放、不雙擊放大 */
}
/* 非遊戲區(設定頁、說明頁)保留縮放能力 */
.settings {
touch-action: manipulation; /* 只拿掉雙擊縮放與 300ms 點擊延遲,保留捏合縮放 */
}touch-action: manipulation 等同 pan-x pan-y pinch-zoom,是無障礙上可接受的預設;none 只用在真正需要接管全部手勢的畫布本身。
四組視口高度單位
CSS Values 4 定義了三種視口尺寸,各自有完整單位家族(*vh、*vw、*vmin、*vmax、以及邏輯軸的 *vi/*vb):
| 單位 | 定義 | 工具列展開時 | 工具列收起時 | 穩定性 |
|---|---|---|---|---|
lvh(large) | 假設所有可收合 UI 都已收起 | 比可視區大 | 剛好 | 固定值 |
svh(small) | 假設所有可收合 UI 都已展開 | 剛好 | 比可視區小 | 固定值 |
dvh(dynamic) | 隨當下 UI 狀態即時變動 | 剛好 | 剛好 | 會變動 |
vh(legacy) | 等同 lvh | 比可視區大 | 剛好 | 固定值 |
三者在桌機與沒有動態 UI 的環境完全相同,所以可以無條件使用。支援度上,svh/lvh/dvh 於 Chrome 108+、Edge 108+、Firefox 101+、Safari 15.4+ 起可用,2025 年 6 月成為 Baseline Widely available。
該用哪一個
dvh 有一個常被忽略的限制:它不保證 60fps 更新。 web.dev 的說明寫得很直接——動態視口的值在 UA 介面展開/收合過程中會被節流,某些瀏覽器甚至會依手勢類型(緩慢捲動 vs 快速滑動)整個 debounce。用 height: 100dvh 當遊戲主容器,在工具列滑出滑入的那 200ms 內會看到畫面高度階梯式跳動,而每次跳動都會觸發你的 resize 重算與 canvas 重建。
所以實務上的分工是:
/* ✅ 遊戲主容器:用 fixed 脫離文件流,高度由 inset 決定,完全不受工具列影響 */
.game-root {
position: fixed;
inset: 0;
}
/* ✅ 需要「一定看得到」的固定 UI:用 svh(最保守的高度) */
.hud-bottom {
bottom: max(env(safe-area-inset-bottom), 8px);
}
/* ✅ 文件型頁面的 hero 區塊:用 dvh,讓它跟著工具列變高變矮觀感最好 */
.landing-hero {
min-height: 100dvh;
}
/* ❌ 遊戲畫布容器不要用 dvh,抖動會傳導成 canvas 重建 */position: fixed; inset: 0 之所以最穩,是因為它的包含塊是佈局視口,且瀏覽器對它的處理已針對工具列狀況特殊化——實測上它比任何 vh 變體都貼合可視區,而且不會在捲動中變動。
dvh 與虛擬鍵盤
dvh 不會因為虛擬鍵盤彈出而變小(在預設的 interactive-widget=resizes-visual 下,鍵盤只縮小視覺視口,佈局視口不動)。想讓 dvh 反映鍵盤,必須改成 interactive-widget=resizes-content;Safari 尚未支援,所以 iOS 上仍得靠 visualViewport 手動補償,做法見 鍵盤與 visualViewport。
一份可直接抄的基準設定
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
<!-- 加到主畫面後以 standalone 執行,是 iPhone 上取得「無工具列滿版」的實際手段 -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#000000">
</head>apple-mobile-web-app-status-bar-style: black-translucent 會讓 standalone PWA 的內容延伸到狀態列底下——這是 iPhone 上少數能真正拿到整塊螢幕的方法,代價是你必須用 env(safe-area-inset-top) 自己避開狀態列。
檢查清單
-
viewport-fit=cover有寫(否則env(safe-area-inset-*)恆為 0) - 沒有用
user-scalable=no當作防縮放手段,改用touch-action - 遊戲主容器用
position: fixed; inset: 0,不是100vh也不是100dvh - 固定 UI 的定位基準用
svh或env(),不用lvh/vh - 有實機在「工具列展開」與「收起」兩種狀態下各測一次
下一篇把這些設定組裝成一個真的不會出捲軸、不會橡皮筋、不會被瀏海切到的容器 → 真正滿版的容器