使用 safe-area-inset 构建移动端安全布局
摘要
一篇文章介绍如何使用 CSS safe-area-inset 环境变量构建移动端布局,以避免被系统 UI 元素(如刘海屏、底部指示条)遮挡。
暂无内容
查看缓存全文
缓存时间: 2026/06/01 01:38
# 使用安全区域插入构建移动安全布局
来源:https://polypane.app/blog/using-safe-area-inset-to-build-mobile-safe-layouts/
现代手机并非简单的矩形。它们有圆角、摄像头挖孔、灵动岛和用作手势区域的底部指示条。浏览器知道所有这些元素的尺寸,并将可能遮挡内容的部分暴露为**安全区域插入(safe area insets)**。
“安全区域”是屏幕上一块保证不会被系统 UI 遮挡的部分。安全区域插入则是系统 UI 在屏幕每条边上占据的空间大小。通过在 CSS 中使用这些值,你可以确保重要内容和控件不会被系统 UI 遮挡。如果你不想让浮动聊天按钮最终出现在底部指示条后面而无法触及,就需要考虑安全区域插入。
## 安全区域插入的环境变量
借助安全区域插入的环境变量,你可以让布局适应当前设备的安全区域,避免这些 bug。`env()` 函数可以让你在 CSS 中读取这些值:
```css
body {
padding-top: env(safe-area-inset-top);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
}
```
### 浏览器支持
安全区域插入属于**基线广泛可用(baseline widely available)**,这意味着你现在就可以在生产环境中使用,并且可以确信它能在几乎所有移动设备用户上正常工作。由于它已是基线广泛可用,你并不真正需要考虑回退方案,但如果你想更加保险,可以在浏览器不支持 `env()` 时提供一个回退 padding:
```css
body {
padding-top: 1rem; /* 对于不支持 env() 的浏览器的回退 */
padding-top: calc(env(safe-area-inset-top) + 1rem);
}
```
而对于支持 `env()` 但不支持 `safe-area-inset-*` 变量的浏览器,你可以直接在 `env()` 函数中提供一个回退值。如果 `safe-area-inset-top` 不被支持,它将回退到 `1rem`:
```css
body {
padding-top: env(safe-area-inset-top, 1rem);
}
```
值得注意的是,这种情况纯粹是理论上的,因为所有支持 `env()` 的浏览器也都支持 `safe-area-inset-*` 变量。但对于其他环境变量可能并非如此,所以知道这种回退机制的存在是件好事。
在深入具体问题区域之前,我们先大致了解一下这些变量如何工作以及如何用它们影响布局。在这个演示中,调整每个插入值,观察一个考虑了安全区域插入的实际移动 UI 如何变化以保持可用性:
- 顶部插入(44px)
- 右侧插入(0px)
- 底部插入(34px)
- 左侧插入(0px)
- 叠加层:显示安全区域
这里的像素值并非针对特定设备,目的在于展示设备如何设置这些变量以及你的布局如何随着它们的变化而响应。在真实设备上,你可以将这些值视为常量。它们由浏览器提供,虽然在横竖屏切换、系统更新改变设备 UI 或不同设备之间可能会变化,但它们不会随着用户滚动或与页面交互而动态变化。浏览器将它们作为常量值提供,你可以用来确保内容不被系统 UI 遮挡。
## 哪些网页实际上需要这样做?
如果你希望页面呈现最佳效果,那么所有页面都需要。默认情况下,浏览器会阻止你的网站被刘海或底部指示条遮挡,因此你的内容无需特殊处理也能安全。但这也有一个缺点,即浏览器会给你一个更小的视口来预留空间:
你会注意到边缘部分能让网站不被遮挡,但看起来并不怎么好。理想情况下,我们希望内容能够边缘到边缘伸展,同时又确保不被系统 UI 遮挡。
要实现这一点,你需要选择全视口并自己处理安全区域。这需要两步。首先,在 meta viewport 标签中添加 `viewport-fit=cover`:
```html
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
```
这将使浏览器将你的页面边缘到边缘拉伸:
如你所见,内容位于灵动岛和底部指示条后面。`viewport-fit=cover` 告诉浏览器你想负责确保内容不被系统 UI 遮挡。
现在,我们可以使用 `env(safe-area-inset-*)` 将这些元素从系统 UI 后面移开:
```css
.content {
padding-right: env(safe-area-inset-right);
padding-left: env(safe-area-inset-left);
}
```
如果你选择全视口,以下是你现在需要考虑的事项,以确保内容不被系统 UI 遮挡:
- 固定头部和导航栏应在上下方留有足够空间以容纳刘海/灵动岛/摄像头挖孔或底部指示条
- 浮动聊天或帮助按钮应保持在安全区域内
- 全屏对话框和抽屉应占据正确高度,不被底部指示条遮挡
- 靠近屏幕角落的地图或视频控件
### 安全区域插入不提供外边距
安全区域插入被定义为系统 UI 正好占据的空间。这意味着它们不会在系统 UI 边缘和你的内容之间提供任何边距。如果你只设置 padding 为安全区域插入值,你的内容将会紧贴安全区域的边缘,也就是紧贴系统 UI。为了增加一些呼吸空间,你可以通过 `calc()` 将你自己的 padding 加在安全区域插入之上。这样你可以确保内容不仅不被遮挡,还有一些空间透透气:
```css
body {
padding-top: calc(env(safe-area-inset-top) + 1rem);
padding-right: calc(env(safe-area-inset-right) + 1rem);
padding-bottom: calc(env(safe-area-inset-bottom) + 1rem);
padding-left: calc(env(safe-area-inset-left) + 1rem);
}
```
## 安全区域插入仅在移动设备上有非零值
`env()` 在各个浏览器和平台上都受支持,但 `safe-area-inset-*` 变量实际上只在移动设备上有非零值。桌面浏览器始终返回 0,因为桌面浏览器没有覆盖在页面上的 UI。只有在移动设备上这些值才非零,你需要考虑它们。
这正是这些 bug 很容易被忽略的原因。如果你在 Chrome 响应式视图中测试,安全区域插入仍然是 0,开发过程中看不到任何问题。在真实设备上测试往往被推到项目最后。到那时,为了适应安全区域而进行布局修改可能代价高昂,bug 就可能溜到生产环境。更糟糕的是,它们通常只影响特定设备上的用户,因此可能会很长一段时间不被发现。
## Polypane 的设备模拟支持安全区域插入
Polypane 是第一个也是唯一一个模拟安全区域插入的桌面浏览器。Polypane 中的每个设备在横竖屏方向都有正确的安全区域插入值。这是 Polypane 用蓝色显示安全区域插入,粉色显示小视口高度差异的情况:
*(图片说明:Polypane 中显示安全区域和小视口叠加层的设备)*
*我们也是唯一模拟 `svh` 的桌面浏览器,但这将是另一篇文章的话题。*
借助 Polypane 的安全区域插入**叠加层可视化**,你可以清晰地看到每个设备上的不安全区域具体位置。
## 解决一个具体问题:浮动按钮
让我们看一个如何使用 `safe-area-inset` 值解决常见问题的具体例子:浮动按钮最终落入底部指示条后面而无法触及。
- 底部插入(34px)
- 右侧插入(0px)
- 按钮偏移(10px)
- 叠加层:显示安全区域
```css
.chat-button {
position: fixed;
right: 10px;
bottom: 10px;
}
```
在“固定位置”和“使用 env()”之间切换定位方式,以对比硬编码偏移和使用 `safe-area-inset-bottom` 与 `safe-area-inset-right` 的偏移。
## `safe-area-max-inset`
除了 `safe-area-inset-*`,规范还描述了 `safe-area-max-inset-*` 变量。这些变量尚未被广泛支持,但值得一提,因为它们的行为略有不同:
- **`safe-area-inset-*`** 提供的是*当前*的插入值。滚动时,浏览器 Chrome 可以折叠,插入值可以缩小到 `0`。你的元素会随之移动。
- **`safe-area-max-inset-*`** 提供的是浏览器可以为该边报告的*最大*插入值。即使浏览器 Chrome 折叠,它也会保持稳定。在你希望有一个不跳动的保留区域时使用它。
拖动或滚动手机来模拟浏览器地址栏的出现和消失。**绿色按钮** 使用 `safe-area-inset-bottom`,跟随当前插入值。**蓝色按钮** 使用 `safe-area-max-inset-bottom`,保持不动。
- 叠加层:显示安全区域
何时使用哪个取决于你的具体情况。对于某些 UI 组件,随着实时视口状态移动是有意义的,比如总是应该刚好位于底部指示条上方的浮动聊天按钮。对于其他东西,比如持久性 Cookie 条或全屏对话框,最好是保留一个稳定的区域,该区域不会在浏览器 Chrome 折叠时移位,这样用户就不会不小心点错按钮。
```css
/* 跟随实时视口状态 */
.floating-cta {
bottom: calc(env(safe-area-inset-bottom) + 1rem);
}
/* 保留稳定的保留区域,不受浏览器 Chrome 影响 */
.persistent-zone {
bottom: calc(env(safe-area-max-inset-bottom) + 1rem);
}
```
### `safe-area-max-inset-*` 的浏览器支持
截至目前,只有 Chromium 实现了 `safe-area-max-inset-*`,所以(移动)Safari 或 Firefox 不支持。这意味着你目前还不能依赖它,应该为其他浏览器提供回退栈:
```css
.bottom-spacer {
padding-bottom: 1rem;
padding-bottom: calc(env(safe-area-inset-bottom) + 1rem);
padding-bottom: calc(env(safe-area-max-inset-bottom, env(safe-area-inset-bottom)) + 1rem);
}
```
注意我们是如何使用 `env()` 回退机制,在 `safe-area-max-inset-bottom` 不被支持时回退到 `safe-area-inset-bottom` 的。
## 在 Polypane 中测试安全区域
如前所述,安全区域 bug 在开发过程中很容易被忽略。Chrome 的响应式视图始终报告插入值为 0,而真实设备测试往往被推到项目最后,此时修复布局问题代价高昂,bug 已经进入生产环境。Polypane 改变了这一点。你可以在桌面上直接测试安全区域插入(以及小视口行为),同时跨多个设备和方向进行测试,作为正常开发工作流程的一部分。
**在开发过程中捕捉真实设备布局故障,而不是在上线之后。**
(图片:Polypane 中显示可视化的安全区域和小视口差异的移动面板)
## 需要记住的重点
安全区域并非针对不常见设备的边缘情况。它们是现代移动设备的现实:带摄像头挖孔、刘海、灵动岛和底部指示条。你的用户就在这些设备上,如果你想给他们最好的体验,就需要确保内容不被系统 UI 遮挡。
确保你的视口设置为 `viewport-fit=cover`,以获得完整的视口,并使用 `env(safe-area-inset-*)`,这样所有内容都可见且可访问。这能为用户提供最好的体验。
立即从 Polypane 开始测试安全区域,在开发过程中捕捉真实设备布局故障,而不是在上线之后。
相似文章
CSS-Tricks 或能成为合作社
本文提议将 CSS-Tricks 转型为合作社,以保留其作为核心Web开发知识基础设施的角色,并对企业所有权模式提出批评。
@rauchg: Jev Generative UI 已在外部分实现
Chris Tate 的实验展示了一个使用 json-render 和 jev 的即时生成式用户界面系统,支持自定义设计系统的组件实时渲染。
Bolt Forge
Bolt Forge是Bolt.new中的一个新代理,运行在开源模型上,为Pro计划用户提供最多50倍的使用量,并在内部基准测试中达到Bolt顶级付费模型的91%水平。
@Shefali__J: 每位开发者都应该知道的Chrome DevTools技巧 保存以备后用
这条推文分享了使用Chrome DevTools的基本技巧,这些技巧对Web开发者很有价值,并建议保存以备日后参考。
@jarredsumner:在Bun的下一个版本中,`Bun.FetchSession` 提供了一个具有独立keepalive连接池的 `fetch` 函数
Bun在其下一个版本中引入了`Bun.FetchSession`,提供了一个具有专用keepalive连接池的fetch函数,以提升性能和资源管理。