CSS env(safe-area-inset-bottom) 怎麼用?viewport-fit 與固定底部導覽列
在 iPhone、iPad 或使用瀏海與圓角螢幕的裝置上,固定在畫面底部的導覽列可能被 Home Indicator 或瀏覽器 UI 蓋住。env(safe-area-inset-bottom) 是瀏覽器提供的 CSS 環境變數,可以把這段安全距離加入元件的 padding 或 margin。
直接答案是:讓底部元件的背景維持貼齊畫面時,把安全區域加到 padding-block-end;讓單一浮動按鈕避開底部時,才把它用在 inset-block-end。 viewport-fit=cover 只負責允許頁面採用 edge-to-edge 版面,不會自動修正鍵盤、捲動容器或錯誤套用 padding 的問題。
safe-area-inset-bottom 到底代表什麼
env() 和 var() 很像,但變數不是由你在 :root 定義,而是由 user agent 根據目前環境提供。safe-area-inset-top、right、bottom、left 分別代表從視埠邊緣到安全區域的距離;在矩形視埠或沒有遮蔽物時,值可能是 0px。
因此,下面兩件事要分開理解:
| 概念 | 它解決的問題 |
|---|---|
| 安全區域 inset | 內容不要被瀏海、圓角、Home Indicator 或特定瀏覽器 UI 遮住 |
動態視埠單位 dvh | 元素高度如何跟著目前可見的視埠變化 |
visualViewport | JavaScript 如何觀察實際可見視埠的尺寸與位移 |
env(safe-area-inset-bottom) 不是一個固定的「iPhone 底部高度」,也不是所有鍵盤問題的通用偵測器。要處理完整的輸入框與虛擬鍵盤流程,仍需針對實際瀏覽器行為測試。
先用 viewport-fit=cover 建立 edge-to-edge 版面
如果你的設計真的要讓背景延伸到整個螢幕,先在 <head> 裡設定:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">WebKit 的說明把 viewport-fit=cover 和 safe area inset 放在同一個 edge-to-edge 流程裡:前者讓頁面使用整個螢幕,後者讓重要內容退回安全範圍。如果你的頁面不需要延伸到邊緣,不要為了追求非零 inset 而盲目加入 cover;先確認版面和背景的預期行為。
固定底部導覽列:安全距離應放在 padding
先建立一個不依賴 JavaScript 的底部導覽列:
<nav class="bottom-nav" aria-label="主要導覽"> <div class="bottom-nav__items"> <a href="/">首頁</a> <a href="/posts/">文章</a> <a href="/about/">關於</a> </div></nav>:root { --bottom-safe-area: env(safe-area-inset-bottom, 0px);}
.bottom-nav { position: fixed; inset-inline: 0; inset-block-end: 0; padding: 0.75rem 1rem calc(0.75rem + var(--bottom-safe-area)); background: Canvas; border-block-start: 1px solid CanvasText;}
.bottom-nav__items { display: flex; justify-content: space-around; min-block-size: 3rem; align-items: center;}這裡把 inset 加在 padding,而不是寫成 inset-block-end: env(...)。導覽列的背景仍然可以延伸到畫面底部,只有內容會往上保留安全距離。env(..., 0px) 的第二個參數是 fallback;它能讓不認識這個環境變數的瀏覽器採用 0px,但不會替沒有安全區域的裝置憑空製造一段距離。
如果你使用固定的設計間距,可以用 max() 確保最小留白:
.floating-action { position: fixed; inset-inline-end: 1rem; inset-block-end: max(1rem, env(safe-area-inset-bottom, 0px));}浮動按鈕的背景不需要貼齊底部時,這種寫法比增加 padding 更直觀;但要注意 max() 是「至少 1rem」,不是把 1rem 和安全區域相加。
固定列存在時,替捲動內容保留空間
只把導覽列往上推,還不代表頁面最後一段內容不會被它蓋住。頁面本身是 scroll container 時,可以把導覽列高度與安全區域寫進 scroll-padding-block-end:
:root { --bottom-nav-height: 4.5rem;}
html { scroll-padding-block-end: calc( var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px) );}scroll-padding 描述 scrollport 可用的視覺範圍,也會影響 fragment navigation 與 scrollIntoView() 的對齊。若真正的問題是目標元素被固定 header 蓋住,則應分別處理上方的 scroll-padding-block-start;不要把所有遮蔽都塞進底部 inset。
若固定列的高度會隨內容變動,--bottom-nav-height 就不能永遠寫死。此時先用 DevTools 確認實際元件高度,再決定要由版面系統提供 CSS 變數,或由 JavaScript 在布局變更時更新變數;安全區域本身仍交給 CSS env() 處理。
橫向螢幕與四個方向一起處理
橫向顯示時,左右安全區域可能比底部更重要。全螢幕 shell 可以保留一個普通間距,再用 max() 和四個 inset 取較大的值:
.edge-to-edge-shell { padding-block-start: max(1rem, env(safe-area-inset-top, 0px)); padding-inline-end: max(1rem, env(safe-area-inset-right, 0px)); padding-block-end: max(1rem, env(safe-area-inset-bottom, 0px)); padding-inline-start: max(1rem, env(safe-area-inset-left, 0px));}這比只寫 padding-bottom 更適合遊戲、影片播放器或橫向 PWA。不過 shell 的 padding 和子元件自己的 safe area padding 不要重複計算;先決定是哪一層負責邊緣留白,再在其他層使用一般間距。
100dvh 與虛擬鍵盤不要混成一件事
100dvh 可以讓全高版面跟著動態視埠調整,但它不會自動替固定底部控制列安排內容空間:
.app-shell { min-block-size: 100dvh; padding-block-end: calc( 4.5rem + env(safe-area-inset-bottom, 0px) );}當輸入框聚焦後出現虛擬鍵盤,請不要直接用 :has(input:focus) 把 safe area 關掉。這個條件只代表焦點,不代表鍵盤一定遮住底部,也可能誤傷使用鍵盤操作或輔助技術的讀者。若頁面需要對鍵盤做 JavaScript 佈局,先用 VisualViewport 觀察實際可見範圍,再針對該瀏覽器的遮蔽行為建立測試;不要假設 safe-area-inset-bottom 能取代這層判斷。
Tailwind CSS 可以先用 arbitrary value
若不想為一次性的元件修改設定檔,可以直接使用 arbitrary value:
<nav class="fixed inset-x-0 bottom-0 pb-[calc(0.75rem+env(safe-area-inset-bottom,0px))]"> 底部導覽</nav>專案裡若多個元件都需要這個模式,建議抽成語意化的 CSS class 或設計 token,避免每個 template 都重複一段難讀的 class。無論使用 Tailwind 或原生 CSS,仍要確認外層 scroll container 有預留完整高度。
值是 0px 或看起來沒作用時怎麼查
先不要用 JavaScript 讀一個不存在的 --safe-area-inset-bottom 自訂屬性;safe-area-inset-bottom 是 user agent 的環境變數,不會自動變成同名 CSS custom property。依序檢查:
<meta name="viewport">是否真的含有預期的viewport-fit,而且頁面是在你要測試的瀏覽模式中載入。env()是否寫在實際影響尺寸的padding、margin、inset或scroll-padding宣告上。- DevTools 的 computed style 是否顯示了包含
env()的最終值;不要只看原始 CSS。 - 裝置是否真的存在需要避開的安全區域;桌面矩形視窗得到
0px是正常結果。 - 固定列所在的元素與真正捲動的元素是否是同一個布局邊界;列的位置正確,不代表內容沒有被覆蓋。
可以用最小頁面重現,而不是先加入 resize listener、捲動修正或多層 wrapper。記錄作業系統、瀏覽器版本、直向/橫向、Safari 分頁或獨立 PWA 模式、鍵盤狀態,以及元件的 computed padding,通常比一張沒有環境資訊的截圖更容易找出差異。
上線前的測試清單
env() 本身已廣泛支援,但安全區域的數值會隨裝置、瀏覽器 UI 和顯示模式改變。至少測試:
- 桌面矩形視窗:安全區域為
0px時,普通間距仍然存在。 - 行動裝置直向與橫向:底部、左右控制項都沒有被遮住。
- 瀏覽器 UI 展開與收合、Safari 分頁與獨立 PWA 模式。
- 最後一個可互動元素:它能被捲到固定列上方,而不是只看到一半。
- 輸入框聚焦、虛擬鍵盤出現與關閉後,頁面沒有多出永久空白。
- 不支援
env()或max()的舊環境:fallback 仍能顯示可操作的版面。
如果問題只出現在特定版本的 Safari 或 PWA,請把它當成需要縮小範圍的瀏覽器相容性案例,保存最小重現與版本資訊,再查 WebKit issue;不要把未驗證的 workaround 寫成所有 iOS 版本都適用的規則。
常見問題
Q: env(safe-area-inset-bottom, 0px) 的 0px 會不會蓋掉真正的安全距離?
A: 不會。只有瀏覽器無法解析環境變數時才使用 fallback;能解析時會使用當下的 inset。若裝置回報的值本來就是 0px,fallback 也不會替它增加距離。
Q: 一定要加 viewport-fit=cover 才能使用 env() 嗎?
A: 不一定,但 edge-to-edge 版面通常需要它來讓內容延伸到整個螢幕,再用 inset 保護重要內容。若頁面本來就讓瀏覽器把內容放在安全區域內,先確認實際需求,不要只為了讓 computed value 非零而加入 cover。
Q: 為什麼把 bottom 改成 env(safe-area-inset-bottom) 後,底部背景出現空隙?
A: 因為你移動的是整個固定元件,背景也會一起離開畫面底部。需要背景滿版時,讓 bottom 維持 0,把 inset 加在內容容器的 padding;只有浮動按鈕才適合直接調整 inset-block-end。
Q: 可以只靠 safe area 解決虛擬鍵盤嗎?
A: 不應這樣假設。safe area 是裝置與 user agent 提供的安全距離,鍵盤還涉及可見視埠、輸入框位置與瀏覽器的 resize/overlay 行為。先用實機重現,再依需要搭配 VisualViewport 和專用布局測試。
參考資料:
WebKit:Designing Websites for iPhone X
回報錯字、失效連結,或告訴我你想看的延伸主題。