2251 字
11 分鐘

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-toprightbottomleft 分別代表從視埠邊緣到安全區域的距離;在矩形視埠或沒有遮蔽物時,值可能是 0px

因此,下面兩件事要分開理解:

概念它解決的問題
安全區域 inset內容不要被瀏海、圓角、Home Indicator 或特定瀏覽器 UI 遮住
動態視埠單位 dvh元素高度如何跟著目前可見的視埠變化
visualViewportJavaScript 如何觀察實際可見視埠的尺寸與位移

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。依序檢查:

  1. <meta name="viewport"> 是否真的含有預期的 viewport-fit,而且頁面是在你要測試的瀏覽模式中載入。
  2. env() 是否寫在實際影響尺寸的 paddingmargininsetscroll-padding 宣告上。
  3. DevTools 的 computed style 是否顯示了包含 env() 的最終值;不要只看原始 CSS。
  4. 裝置是否真的存在需要避開的安全區域;桌面矩形視窗得到 0px 是正常結果。
  5. 固定列所在的元素與真正捲動的元素是否是同一個布局邊界;列的位置正確,不代表內容沒有被覆蓋。

可以用最小頁面重現,而不是先加入 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 和專用布局測試。

參考資料:

MDN:env() CSS function

WebKit:Designing Websites for iPhone X

Apple WWDC21:Design for Safari 15

MDN:VisualViewport

CSS env(safe-area-inset-bottom) 怎麼用?viewport-fit 與固定底部導覽列
https://laplusda.com/posts/env-safe-area-inset-bottom-guide/
作者
Zero
發佈於
2025-09-17
許可協議
CC BY-NC-SA 4.0
這篇文章有幫助嗎?

回報錯字、失效連結,或告訴我你想看的延伸主題。