safe-area-handling

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Safe Area Handling in Capacitor

Capacitor应用中的安全区域处理

Handle iPhone notch, Dynamic Island, home indicator, and Android cutouts properly.
正确处理iPhone刘海屏、Dynamic Island、Home指示器及Android挖孔屏的布局适配问题。

When to Use This Skill

何时使用此技能

  • User has layout issues on notched devices
  • User asks about safe areas
  • User sees content under the notch
  • User needs fullscreen layout
  • Content is hidden by home indicator
  • 用户在带刘海/挖孔的设备上遇到布局问题
  • 用户询问安全区域相关问题
  • 用户发现内容被刘海遮挡
  • 用户需要全屏布局方案
  • 内容被Home指示器遮挡

Understanding Safe Areas

理解安全区域

What Are Safe Areas?

什么是安全区域?

Safe areas are the regions of the screen not obscured by:
  • iPhone: Notch, Dynamic Island, home indicator, rounded corners
  • Android: Camera cutouts, navigation gestures, display cutouts
安全区域是指屏幕上未被以下元素遮挡的区域:
  • iPhone:刘海屏、Dynamic Island、Home指示器、圆角
  • Android:摄像头挖孔、导航手势区域、显示挖孔

Safe Area Insets

安全区域内边距

InsetDescription
safe-area-inset-top
Notch/Dynamic Island/status bar
safe-area-inset-bottom
Home indicator/navigation bar
safe-area-inset-left
Left edge (landscape)
safe-area-inset-right
Right edge (landscape)
内边距描述
safe-area-inset-top
刘海/Dynamic Island/状态栏区域
safe-area-inset-bottom
Home指示器/导航栏区域
safe-area-inset-left
左侧边缘(横屏模式)
safe-area-inset-right
右侧边缘(横屏模式)

CSS Solution

CSS解决方案

Enable Viewport Coverage

启用视口全覆盖

html
<!-- index.html -->
<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
Important:
viewport-fit=cover
is required to access safe area insets.
html
<!-- index.html -->
<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
重要提示:必须设置
viewport-fit=cover
才能访问安全区域内边距。

Using CSS Environment Variables

使用CSS环境变量

css
/* Basic usage */
.header {
  padding-top: env(safe-area-inset-top);
}

.footer {
  padding-bottom: env(safe-area-inset-bottom);
}

/* With fallback */
.header {
  padding-top: env(safe-area-inset-top, 20px);
}

/* Combined with other padding */
.content {
  padding-top: calc(env(safe-area-inset-top) + 16px);
  padding-bottom: calc(env(safe-area-inset-bottom) + 16px);
}
css
/* 基础用法 */
.header {
  padding-top: env(safe-area-inset-top);
}

.footer {
  padding-bottom: env(safe-area-inset-bottom);
}

/* 带回退值的用法 */
.header {
  padding-top: env(safe-area-inset-top, 20px);
}

/* 与其他内边距组合使用 */
.content {
  padding-top: calc(env(safe-area-inset-top) + 16px);
  padding-bottom: calc(env(safe-area-inset-bottom) + 16px);
}

Full Page Layout

整页布局方案

css
/* App container */
.app {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
}

/* Header respects notch */
.header {
  padding-top: env(safe-area-inset-top);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  background: #fff;
}

/* Scrollable content */
.content {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

/* Footer respects home indicator */
.footer {
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  background: #fff;
}
css
/* 应用容器 */
.app {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
}

/* 适配刘海的头部 */
.header {
  padding-top: env(safe-area-inset-top);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  background: #fff;
}

/* 可滚动内容区 */
.content {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

/* 适配Home指示器的底部 */
.footer {
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  background: #fff;
}

Tab Bar with Safe Area

适配安全区域的标签栏

css
.tab-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  background: #fff;
  border-top: 1px solid #eee;

  /* Add padding for home indicator */
  padding-bottom: env(safe-area-inset-bottom);
}

.tab-bar-item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 8px 0;
  min-height: 49px; /* iOS standard height */
}
css
.tab-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  background: #fff;
  border-top: 1px solid #eee;

  /* 添加Home指示器的内边距 */
  padding-bottom: env(safe-area-inset-bottom);
}

.tab-bar-item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 8px 0;
  min-height: 49px; /* iOS标准高度 */
}

Full-Bleed Background with Safe Content

全屏背景+安全区域内容

css
.hero {
  /* Background extends to edges */
  background: linear-gradient(to bottom, #4f46e5, #7c3aed);
  padding-top: calc(env(safe-area-inset-top) + 20px);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

.hero-content {
  /* Content stays in safe area */
  max-width: 100%;
}
css
.hero {
  /* 背景延伸至屏幕边缘 */
  background: linear-gradient(to bottom, #4f46e5, #7c3aed);
  padding-top: calc(env(safe-area-inset-top) + 20px);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

.hero-content {
  /* 内容保持在安全区域内 */
  max-width: 100%;
}

JavaScript Solution

JavaScript解决方案

Reading Safe Area Values

读取安全区域值

typescript
function getSafeAreaInsets() {
  const computedStyle = getComputedStyle(document.documentElement);

  return {
    top: parseInt(computedStyle.getPropertyValue('--sat') || '0'),
    bottom: parseInt(computedStyle.getPropertyValue('--sab') || '0'),
    left: parseInt(computedStyle.getPropertyValue('--sal') || '0'),
    right: parseInt(computedStyle.getPropertyValue('--sar') || '0'),
  };
}

// Set CSS custom properties
function setSafeAreaProperties() {
  const style = document.documentElement.style;

  // Create temporary element to read values
  const temp = document.createElement('div');
  temp.style.paddingTop = 'env(safe-area-inset-top)';
  temp.style.paddingBottom = 'env(safe-area-inset-bottom)';
  temp.style.paddingLeft = 'env(safe-area-inset-left)';
  temp.style.paddingRight = 'env(safe-area-inset-right)';
  document.body.appendChild(temp);

  const computed = getComputedStyle(temp);
  style.setProperty('--sat', computed.paddingTop);
  style.setProperty('--sab', computed.paddingBottom);
  style.setProperty('--sal', computed.paddingLeft);
  style.setProperty('--sar', computed.paddingRight);

  document.body.removeChild(temp);
}

// Update on orientation change
window.addEventListener('orientationchange', () => {
  setTimeout(setSafeAreaProperties, 100);
});
typescript
function getSafeAreaInsets() {
  const computedStyle = getComputedStyle(document.documentElement);

  return {
    top: parseInt(computedStyle.getPropertyValue('--sat') || '0'),
    bottom: parseInt(computedStyle.getPropertyValue('--sab') || '0'),
    left: parseInt(computedStyle.getPropertyValue('--sal') || '0'),
    right: parseInt(computedStyle.getPropertyValue('--sar') || '0'),
  };
}

// 设置CSS自定义属性
function setSafeAreaProperties() {
  const style = document.documentElement.style;

  // 创建临时元素以读取值
  const temp = document.createElement('div');
  temp.style.paddingTop = 'env(safe-area-inset-top)';
  temp.style.paddingBottom = 'env(safe-area-inset-bottom)';
  temp.style.paddingLeft = 'env(safe-area-inset-left)';
  temp.style.paddingRight = 'env(safe-area-inset-right)';
  document.body.appendChild(temp);

  const computed = getComputedStyle(temp);
  style.setProperty('--sat', computed.paddingTop);
  style.setProperty('--sab', computed.paddingBottom);
  style.setProperty('--sal', computed.paddingLeft);
  style.setProperty('--sar', computed.paddingRight);

  document.body.removeChild(temp);
}

// 屏幕旋转时更新
window.addEventListener('orientationchange', () => {
  setTimeout(setSafeAreaProperties, 100);
});

React Hook

React Hook

typescript
import { useState, useEffect } from 'react';

interface SafeAreaInsets {
  top: number;
  bottom: number;
  left: number;
  right: number;
}

function useSafeArea(): SafeAreaInsets {
  const [insets, setInsets] = useState<SafeAreaInsets>({
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
  });

  useEffect(() => {
    function updateInsets() {
      const temp = document.createElement('div');
      temp.style.cssText = `
        position: fixed;
        top: 0;
        padding-top: env(safe-area-inset-top);
        padding-bottom: env(safe-area-inset-bottom);
        padding-left: env(safe-area-inset-left);
        padding-right: env(safe-area-inset-right);
      `;
      document.body.appendChild(temp);

      const computed = getComputedStyle(temp);
      setInsets({
        top: parseFloat(computed.paddingTop) || 0,
        bottom: parseFloat(computed.paddingBottom) || 0,
        left: parseFloat(computed.paddingLeft) || 0,
        right: parseFloat(computed.paddingRight) || 0,
      });

      document.body.removeChild(temp);
    }

    updateInsets();
    window.addEventListener('resize', updateInsets);
    window.addEventListener('orientationchange', () => {
      setTimeout(updateInsets, 100);
    });

    return () => {
      window.removeEventListener('resize', updateInsets);
    };
  }, []);

  return insets;
}

// Usage
function Header() {
  const { top } = useSafeArea();

  return (
    <header style={{ paddingTop: top }}>
      App Header
    </header>
  );
}
typescript
import { useState, useEffect } from 'react';

interface SafeAreaInsets {
  top: number;
  bottom: number;
  left: number;
  right: number;
}

function useSafeArea(): SafeAreaInsets {
  const [insets, setInsets] = useState<SafeAreaInsets>({
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
  });

  useEffect(() => {
    function updateInsets() {
      const temp = document.createElement('div');
      temp.style.cssText = `
        position: fixed;
        top: 0;
        padding-top: env(safe-area-inset-top);
        padding-bottom: env(safe-area-inset-bottom);
        padding-left: env(safe-area-inset-left);
        padding-right: env(safe-area-inset-right);
      `;
      document.body.appendChild(temp);

      const computed = getComputedStyle(temp);
      setInsets({
        top: parseFloat(computed.paddingTop) || 0,
        bottom: parseFloat(computed.paddingBottom) || 0,
        left: parseFloat(computed.paddingLeft) || 0,
        right: parseFloat(computed.paddingRight) || 0,
      });

      document.body.removeChild(temp);
    }

    updateInsets();
    window.addEventListener('resize', updateInsets);
    window.addEventListener('orientationchange', () => {
      setTimeout(updateInsets, 100);
    });

    return () => {
      window.removeEventListener('resize', updateInsets);
    };
  }, []);

  return insets;
}

// 使用示例
function Header() {
  const { top } = useSafeArea();

  return (
    <header style={{ paddingTop: top }}>
      应用头部
    </header>
  );
}

Vue Composable

Vue 组合式函数

typescript
import { ref, onMounted, onUnmounted } from 'vue';

export function useSafeArea() {
  const insets = ref({
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
  });

  function updateInsets() {
    const temp = document.createElement('div');
    temp.style.cssText = `
      position: fixed;
      padding-top: env(safe-area-inset-top);
      padding-bottom: env(safe-area-inset-bottom);
      padding-left: env(safe-area-inset-left);
      padding-right: env(safe-area-inset-right);
    `;
    document.body.appendChild(temp);

    const computed = getComputedStyle(temp);
    insets.value = {
      top: parseFloat(computed.paddingTop) || 0,
      bottom: parseFloat(computed.paddingBottom) || 0,
      left: parseFloat(computed.paddingLeft) || 0,
      right: parseFloat(computed.paddingRight) || 0,
    };

    document.body.removeChild(temp);
  }

  onMounted(() => {
    updateInsets();
    window.addEventListener('resize', updateInsets);
  });

  onUnmounted(() => {
    window.removeEventListener('resize', updateInsets);
  });

  return insets;
}
typescript
import { ref, onMounted, onUnmounted } from 'vue';

export function useSafeArea() {
  const insets = ref({
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
  });

  function updateInsets() {
    const temp = document.createElement('div');
    temp.style.cssText = `
      position: fixed;
      padding-top: env(safe-area-inset-top);
      padding-bottom: env(safe-area-inset-bottom);
      padding-left: env(safe-area-inset-left);
      padding-right: env(safe-area-inset-right);
    `;
    document.body.appendChild(temp);

    const computed = getComputedStyle(temp);
    insets.value = {
      top: parseFloat(computed.paddingTop) || 0,
      bottom: parseFloat(computed.paddingBottom) || 0,
      left: parseFloat(computed.paddingLeft) || 0,
      right: parseFloat(computed.paddingRight) || 0,
    };

    document.body.removeChild(temp);
  }

  onMounted(() => {
    updateInsets();
    window.addEventListener('resize', updateInsets);
  });

  onUnmounted(() => {
    window.removeEventListener('resize', updateInsets);
  });

  return insets;
}

Native iOS Configuration

iOS原生配置

Status Bar Style

状态栏样式

typescript
// capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  ios: {
    // Content extends behind status bar
    contentInset: 'automatic', // or 'always', 'scrollableAxes', 'never'
  },
};
typescript
// capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  ios: {
    // 内容延伸至状态栏后方
    contentInset: 'automatic', // 可选值:'always', 'scrollableAxes', 'never'
  },
};

Extend Behind Safe Areas

延伸内容至安全区域外

swift
// ios/App/App/AppDelegate.swift
import UIKit
import Capacitor

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Extend content to edges
        if let window = UIApplication.shared.windows.first {
            window.backgroundColor = .clear
        }
        return true
    }
}
swift
// ios/App/App/AppDelegate.swift
import UIKit
import Capacitor

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // 将内容延伸至屏幕边缘
        if let window = UIApplication.shared.windows.first {
            window.backgroundColor = .clear
        }
        return true
    }
}

Info.plist Settings

Info.plist 设置

xml
<!-- ios/App/App/Info.plist -->
<!-- Allow full screen content -->
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>

<!-- For landscape support -->
<key>UISupportedInterfaceOrientations</key>
<array>
    <string>UIInterfaceOrientationPortrait</string>
    <string>UIInterfaceOrientationLandscapeLeft</string>
    <string>UIInterfaceOrientationLandscapeRight</string>
</array>
xml
<!-- ios/App/App/Info.plist -->
<!-- 允许全屏内容 -->
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>

<!-- 支持横屏 -->
<key>UISupportedInterfaceOrientations</key>
<array>
    <string>UIInterfaceOrientationPortrait</string>
    <string>UIInterfaceOrientationLandscapeLeft</string>
    <string>UIInterfaceOrientationLandscapeRight</string>
</array>

Native Android Configuration

Android原生配置

Display Cutout Mode

显示挖孔模式

xml
<!-- android/app/src/main/res/values-v28/styles.xml -->
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
        <!-- Extend content into cutout area -->
        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
    </style>
</resources>
xml
<!-- android/app/src/main/res/values-v28/styles.xml -->
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
        <!-- 将内容延伸至挖孔区域 -->
        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
    </style>
</resources>

Edge-to-Edge Display

全屏显示(边缘到边缘)

kotlin
// android/app/src/main/java/.../MainActivity.kt
import android.os.Build
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController

class MainActivity : BridgeActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Enable edge-to-edge
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            window.setDecorFitsSystemWindows(false)
        } else {
            @Suppress("DEPRECATION")
            window.decorView.systemUiVisibility = (
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
                View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
                View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
            )
        }
    }
}
kotlin
// android/app/src/main/java/.../MainActivity.kt
import android.os.Build
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController

class MainActivity : BridgeActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 启用边缘到边缘显示
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            window.setDecorFitsSystemWindows(false)
        } else {
            @Suppress("DEPRECATION")
            window.decorView.systemUiVisibility = (
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
                View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
                View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
            )
        }
    }
}

AndroidManifest Configuration

AndroidManifest 配置

xml
<!-- android/app/src/main/AndroidManifest.xml -->
<activity
    android:name=".MainActivity"
    android:theme="@style/AppTheme"
    android:windowSoftInputMode="adjustResize"
    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode">
</activity>
xml
<!-- android/app/src/main/AndroidManifest.xml -->
<activity
    android:name=".MainActivity"
    android:theme="@style/AppTheme"
    android:windowSoftInputMode="adjustResize"
    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode">
</activity>

Capacitor Status Bar Plugin

Capacitor状态栏插件

Installation

安装

bash
bun add @capacitor/status-bar
bunx cap sync
bash
bun add @capacitor/status-bar
bunx cap sync

Usage

使用

typescript
import { StatusBar, Style } from '@capacitor/status-bar';

// Set status bar style
await StatusBar.setStyle({ style: Style.Dark });

// Set background color (Android)
await StatusBar.setBackgroundColor({ color: '#ffffff' });

// Show/hide status bar
await StatusBar.hide();
await StatusBar.show();

// Overlay mode
await StatusBar.setOverlaysWebView({ overlay: true });
typescript
import { StatusBar, Style } from '@capacitor/status-bar';

// 设置状态栏样式
await StatusBar.setStyle({ style: Style.Dark });

// 设置背景色(Android)
await StatusBar.setBackgroundColor({ color: '#ffffff' });

// 显示/隐藏状态栏
await StatusBar.hide();
await StatusBar.show();

// 覆盖模式
await StatusBar.setOverlaysWebView({ overlay: true });

Common Issues and Solutions

常见问题与解决方案

Issue: Content Behind Notch

问题:内容被刘海遮挡

Solution: Add viewport-fit and safe area padding
html
<meta name="viewport" content="viewport-fit=cover">
css
body {
  padding-top: env(safe-area-inset-top);
}
解决方案:添加viewport-fit和安全区域内边距
html
<meta name="viewport" content="viewport-fit=cover">
css
body {
  padding-top: env(safe-area-inset-top);
}

Issue: Tab Bar Under Home Indicator

问题:标签栏被Home指示器遮挡

Solution: Add bottom safe area padding
css
.tab-bar {
  padding-bottom: env(safe-area-inset-bottom);
}
解决方案:添加底部安全区域内边距
css
.tab-bar {
  padding-bottom: env(safe-area-inset-bottom);
}

Issue: Landscape Layout Broken

问题:横屏布局损坏

Solution: Handle left/right insets
css
.content {
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}
解决方案:处理左右内边距
css
.content {
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

Issue: Keyboard Pushes Content

问题:键盘弹出时内容被顶起

Solution: Use adjustResize and handle insets dynamically
typescript
import { Keyboard } from '@capacitor/keyboard';

Keyboard.addListener('keyboardWillShow', (info) => {
  document.body.style.paddingBottom = `${info.keyboardHeight}px`;
});

Keyboard.addListener('keyboardWillHide', () => {
  document.body.style.paddingBottom = 'env(safe-area-inset-bottom)';
});
解决方案:使用adjustResize并动态处理内边距
typescript
import { Keyboard } from '@capacitor/keyboard';

Keyboard.addListener('keyboardWillShow', (info) => {
  document.body.style.paddingBottom = `${info.keyboardHeight}px`;
});

Keyboard.addListener('keyboardWillHide', () => {
  document.body.style.paddingBottom = 'env(safe-area-inset-bottom)';
});

Issue: Safe Areas Not Working in WebView

问题:WebView中安全区域不生效

Cause: Missing viewport-fit=cover
Solution:
html
<!-- Must be exactly like this -->
<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
原因:缺少viewport-fit=cover设置
解决方案
html
<!-- 必须严格按照此格式设置 -->
<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>

Testing Safe Areas

测试安全区域

iOS Simulator

iOS模拟器

  1. Use iPhone with notch (iPhone 14 Pro, etc.)
  2. Test both portrait and landscape
  3. Test with keyboard visible
  1. 使用带刘海的iPhone设备(如iPhone 14 Pro等)
  2. 测试竖屏和横屏模式
  3. 测试键盘弹出时的布局

Android Emulator

Android模拟器

  1. Create emulator with camera cutout
  2. Test navigation gesture mode
  3. Test 3-button navigation mode
  1. 创建带摄像头挖孔的模拟器
  2. 测试导航手势模式
  3. 测试三键导航模式

Preview Different Devices

预览不同设备的安全区域

css
/* Debug mode - visualize safe areas */
.debug-safe-areas::before {
  content: '';
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: env(safe-area-inset-top);
  background: rgba(255, 0, 0, 0.3);
  z-index: 9999;
  pointer-events: none;
}

.debug-safe-areas::after {
  content: '';
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: env(safe-area-inset-bottom);
  background: rgba(0, 0, 255, 0.3);
  z-index: 9999;
  pointer-events: none;
}
css
/* 调试模式 - 可视化安全区域 */
.debug-safe-areas::before {
  content: '';
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: env(safe-area-inset-top);
  background: rgba(255, 0, 0, 0.3);
  z-index: 9999;
  pointer-events: none;
}

.debug-safe-areas::after {
  content: '';
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: env(safe-area-inset-bottom);
  background: rgba(0, 0, 255, 0.3);
  z-index: 9999;
  pointer-events: none;
}

Resources

参考资源