Web 模态框
编辑页面
了解如何使用 Expo Router 在你的 web 应用中实现并自定义模态框的行为。
For the complete documentation index, see llms.txt. Use this file to discover all available pages.
重要 Web 模态框处于 alpha 阶段,并可在 SDK 54 及更高版本中使用。要使用此功能,你必须在项目中设置
EXPO_UNSTABLE_WEB_MODAL=1环境变量。
现代 Web 应用需要一种灵活的模态体验,以适应不同的内容大小和用户交互。Expo Router 为现代 Web 体验提供了多种模态展示模式。这些模式利用 presentation 配合 modal、formSheet、transparentModal 或 containedTransparentModal 来呈现基于不同屏幕宽度的模态,并使用 webModalStyle 提供可自定义的样式属性。
开始使用
提示 要使用新的 Web 模态功能,你必须在开发和 导出 构建中都设置
EXPO_UNSTABLE_WEB_MODAL=1环境变量。你可以通过将其添加到项目根目录的 .env 文件中,或者在命令前加上前缀来实现,例如:EXPO_UNSTABLE_WEB_MODAL=1 npx expo start。
Expo Router 中的模态通过 Stack.Screen 组件及特定选项进行配置。这要求将模态屏幕添加到应用 Stack 的布局文件中。
考虑以下导航树,其中包括在布局文件中定义的堆栈导航器、访问模态的主页,以及模态屏幕组件:
srcapp_layout.tsxindex.tsxmodal.tsx在布局文件(src/app/_layout.tsx)中,模态屏幕组件被添加到 Stack 导航器:
import { Stack } from 'expo-router'; export const unstable_settings = { anchor: 'index', }; export default function Layout() { return ( <Stack> <Stack.Screen name="index" /> <Stack.Screen name="modal" options={{ presentation: 'modal', // 启用模态行为 sheetAllowedDetents: [0.5, 1], // 对于宽度小于 768px 的屏幕,数组形式的吸附位置。 }} /> </Stack> ); }
modal.tsx 用于显示模态的内容:
import { Text, View } from 'react-native'; export default function Modal() { return <View style={{ flex: 1, padding: 16 }}>{/* 模态内容放在这里 */}</View>; }
现在,要从 index.tsx 打开模态,你可以在 index 路由中使用 router.push('/modal'):
import { router } from 'expo-router'; import { Pressable, Text, View, StyleSheet } from 'react-native'; export default function Home() { return ( <View style={styles.container}> <Text style={styles.title}>主页</Text> <Pressable onPress={() => router.push('/modal')} style={styles.button}> <Text style={styles.buttonText}>打开模态</Text> </Pressable> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, title: { fontSize: 24, fontWeight: 'bold', marginBottom: 20, }, button: { backgroundColor: '#007AFF', padding: 16, borderRadius: 8, }, buttonText: { color: 'white', fontSize: 16, fontWeight: '600', }, });
以下是上述示例的效果:
锚点和嵌套堆栈
在使用堆栈或嵌套堆栈导航器时,模态需要正确地锚定,以确保导航行为正确,尤其是在深度链接到模态路由时。如果没有锚定,模态后面的屏幕会被清除,从而失去导航上下文。
锚点 充当模态的基础。在复杂应用中,当你有嵌套堆栈时,必须为嵌套堆栈定义锚点,其值会成为该堆栈的初始路由。
你可以通过从堆栈的布局文件中导出 unable_settings 来配置锚点:
export const unstable_settings = { anchor: 'index', // 锚定到 index 路由 };
在上面的示例中,anchor: 'index' 告诉 Expo Router,在展示模态时应在后台保留指定的锚点路由。
模态展示样式
在你的 Web 应用中,模态在大屏幕上(例如桌面端)的呈现方式,与 Web 应用在移动设备上运行时保持抽屉式行为之间的差异,取决于配置选项。以下是可用于配置 Web 模态外观的选项,这些选项可以传递给 Stack.Screen 的 options 对象。
| 选项 | 类型 | 描述 |
|---|---|---|
presentation | 'modal', 'formSheet', 'transparentModal', 'containedTransparentModal' | 模态展示样式。在宽度大于 768px 的屏幕上,所有样式都会显示为居中的覆盖层(例如,类似 lightbox)。 在宽度小于 768px 的屏幕上,formSheet 会以底部抽屉的形式显示。当设置为 transparentModal 时,它会显示为覆盖层,但不会完全遮挡背景内容。不会应用 detents 和 sheet 抓手属性。此展示方式适合构建你自己的自定义模态。与 transparentModal 类似,当设置为 containedTransparentModal 时,它会显示为覆盖层,但不会完全遮挡背景内容。不会应用 detents 和其他属性。此展示方式适合构建你自己的自定义模态。 |
sheetAllowedDetents | number[], 'fitToContents' | 以百分比(0.0-1.0)表示的吸附位置,或自动适配内容。仅适用于宽度小于 768px 的屏幕。 |
sheetGrabberVisible | boolean | **在 iOS 上,**显示/隐藏抽屉顶部的拖拽把手。不支持 Android 和 web。我们建议使用自定义的抽屉头部组件,以在所有平台上模拟这个把手。 |
sheetCornerRadius | number | 抽屉的圆角半径(像素)。 |
webModalStyle | WebModalStyle | 特殊属性,允许使用仅限 Web 的样式选项来微调模态外观。 |
使用 webModalStyle 自定义模态样式
提示 注意:
webModalStyle属性仅适用于 web 平台。在移动端,模态会自动适配为适合触控交互的 sheet 行为。
你可以使用 webModalStyle 自定义 web 端模态的尺寸和外观。它提供了以下属性以便进一步定制:
| 属性 | 类型 | 描述 | 默认值 |
|---|---|---|---|
width | number string | 覆盖模态的宽度(px 或百分比)。仅适用于桌面端的 web 平台。 | 83vw |
height | number string | 覆盖模态的高度(px 或百分比)。仅适用于桌面端的 web 平台。 | 79vh |
minHeight | number string | 桌面端模态的最小高度(px 或百分比)。覆盖默认的 iOS 26 尺寸。 | min(586px, 79vh) |
minWidth | number string | 桌面端模态的最小宽度(px 或百分比)。覆盖默认的 iOS 26 尺寸。 | min(936px, 83vw) |
border | string | 覆盖桌面端模态的边框(任何有效的 CSS border 值,例如 '1px solid #ccc' 或 'none') | 无 |
overlayBackground | string | 覆盖遮罩层背景颜色(任何有效的 CSS 颜色或 rgba/hsla 值)。 | 半透明黑色 |
shadow | string | 覆盖模态的阴影滤镜(任何有效的 CSS filter 值,例如 'drop-shadow(0 4px 8px rgba(0,0,0,0.1))' 或 'none') | 投影阴影滤镜 |
自定义 CSS 属性
Expo Router 使用自定义 CSS 属性来设置模态样式,你可以通过 webModalStyle 全局覆盖这些属性。这些变量可以对模态的外观进行细粒度控制。
宽度和高度尺寸变量
/* 默认模态宽度(桌面端为 83vw,遵循 iOS 26 规范) */ --expo-router-modal-width: 83vw; /* 最大模态宽度(最大 936px,默认 83vw,遵循 iOS 26) */ --expo-router-modal-max-width: min(936px, 83vw); /* 最小模态宽度(默认 auto) */ --expo-router-modal-min-width: auto; /* 默认模态高度(79vh,遵循 iOS 26 规范) */ --expo-router-modal-height: 79vh; /* 最小模态高度(最大 586px,默认 79vh,遵循 iOS 26) */ --expo-router-modal-min-height: min(586px, 79vh);
边框和遮罩层样式变量
/* modal 边框(默认 none) */ --expo-router-modal-border: none; /* modal 圆角(默认 24px,遵循 iOS 26) */ --expo-router-modal-border-radius: 24px; /* modal 阴影滤镜(默认 drop-shadow) */ --expo-router-modal-shadow: drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1)); /* 遮罩层背景颜色(默认 25% 黑色) */ --expo-router-modal-overlay-background: rgba(0, 0, 0, 0.25);
webModalStyle 如何映射到 CSS 变量
当你使用 webModalStyle 覆盖任意尺寸变量时,Expo Router 会自动将这些 CSS 变量设置为你提供的值:
// 此 webModalStyle 配置 webModalStyle: { width: 800, height: 600, border: '2px solid blue', overlayBackground: 'rgba(0, 0, 0, 0.7)', shadow: 'drop-shadow(0 8px 16px rgba(0,0,0,0.2))', } // ...会自动设置这些 CSS 变量: // --expo-router-modal-width: 800px // --expo-router-modal-height: 600px // --expo-router-modal-border: 2px solid blue // --expo-router-modal-overlay-background: rgba(0, 0, 0, 0.7) // --expo-router-modal-shadow: drop-shadow(0 8px 16px rgba(0,0,0,0.2))
Common Examples
Full-screen modal example
If you want to create a full-screen modal for content that covers the maximum space, you can use the webModalStyle property in the modal route's Stack.Screen options:
import { Stack } from 'expo-router'; export const unstable_settings = { anchor: 'index', }; export default function Layout() { return ( <Stack> <Stack.Screen name="index" /> <Stack.Screen name="modal" options={{ presentation: 'modal', webModalStyle: { width: '95vw', height: '95vh', border: 'none', }, }} /> </Stack> ); }
The effect of the above example is as follows:
When running a web app on a mobile device, if you want to avoid showing a full-screen modal, you can set sheetAllowedDetents to fitToContents or a custom value:
import { Stack } from 'expo-router'; export const unstable_settings = { anchor: 'index', }; export default function Layout() { return ( <Stack> <Stack.Screen name="index" /> <Stack.Screen name="modal" options={{ presentation: 'modal', webModalStyle: { width: '95vw', height: '95vh', border: 'none', }, sheetAllowedDetents: 'fitToContents', }} /> </Stack> ); }
On mobile devices, the modal will be displayed as a sheet:
Compact modal example
For smaller interactions, you can create a compact modal that adapts to its content:
import { Stack } from 'expo-router'; export const unstable_settings = { anchor: 'index', }; export default function Layout() { return ( <Stack> <Stack.Screen name="index" /> <Stack.Screen name="modal" options={{ presentation: 'modal', webModalStyle: { width: 400, height: 'auto', minHeight: 200, border: '1px solid #e5e7eb', overlayBackground: 'rgba(0, 0, 0, 0.3)', }, sheetCornerRadius: 12, sheetAllowedDetents: 'fitToContents', }} /> </Stack> ); }
The effect of the above example is as follows:
Transparent modal example
When you want to display an overlay and preserve the visual context of the underlying screen, you can set the presentation option to transparentModal:
import { Stack } from 'expo-router'; export const unstable_settings = { anchor: 'index', }; export default function Layout() { return ( <Stack> <Stack.Screen name="index" /> <Stack.Screen name="modal" options={{ presentation: 'transparentModal', }} /> </Stack> ); }
The effect of the above example is as follows:
Rounded corners example
You can use sheetCornerRadius to customize rounded corners:
import { Stack } from 'expo-router'; export const unstable_settings = { anchor: 'index', }; export default function Layout() { return ( <Stack> <Stack.Screen name="index" /> <Stack.Screen name="modal" options={{ presentation: 'formSheet', sheetAllowedDetents: [0.4], sheetCornerRadius: 32, }} /> </Stack> ); }
The effect of the above example is as follows:
Custom detents example
You can use sheetAllowedDetents to define the heights at which the modal can stop:
import { Stack } from 'expo-router'; export const unstable_settings = { anchor: 'index', }; export default function Layout() { return ( <Stack> <Stack.Screen name="index" /> <Stack.Screen name="modal" options={{ presentation: 'formSheet', sheetAllowedDetents: [0.2, 0.5, 0.8, 0.98], }} /> </Stack> ); }
The effect of the above example is as follows:
Global CSS Customization
For your web app, if the project uses a global CSS file, you can also override the width, height, border, and overlay variables.
You can add custom values in a global CSS file using --expo-router-* variables:
/* Globally override the default modal styles */ :root { --expo-router-modal-width: 700px; --expo-router-modal-min-width: auto; --expo-router-modal-max-width: 95vw; --expo-router-modal-height: 640px; --expo-router-modal-min-height: 640px; --expo-router-modal-border: none; --expo-router-modal-border-radius: 16px; --expo-router-modal-shadow: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.2)); --expo-router-modal-overlay-background: rgba(0, 0, 0, 0.5); }
Custom modal route implementation
上方视频演示了一个显示在网页主内容之上的模态窗口。背景会变暗以将注意力吸引到模态窗口上,模态窗口中包含用户所需的信息。这是网页模态窗口的典型行为,用户可以与模态窗口交互,或将其关闭以返回主页面。
你可以通过使用 transparentModal 展示模式、为覆盖层和模态内容设置样式,以及利用 react-native-reanimated 来为模态窗口的展示添加动画,从而实现上述网页模态行为。
修改项目的根布局(src/app/_layout.tsx),为模态路由添加一个 options 对象:
import { Stack } from 'expo-router'; export const unstable_settings = { initialRouteName: 'index', }; export default function Layout() { return ( <Stack> <Stack.Screen name="index" /> <Stack.Screen name="modal" options={{ presentation: 'transparentModal', animation: 'fade', headerShown: false, }} /> </Stack> ); }
注意:unstable_settings当前仅适用于Stack导航器。
上面的示例使用 unstable_settings 将 index 屏幕设置为 initialRouteName。这确保透明模态始终渲染在当前屏幕之上,即使用户通过直接链接导航到模态屏幕也是如此。
在 modal.tsx 中为覆盖层和模态内容设置样式,如下所示:
import { Link } from 'expo-router'; import { Pressable, StyleSheet, Text } from 'react-native'; import Animated, { FadeIn, SlideInDown } from 'react-native-reanimated'; export default function Modal() { return ( <Animated.View entering={FadeIn} style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#00000040', }} > {/* 点击外部区域时关闭模态框 */} <Link href={'/'} asChild> <Pressable style={StyleSheet.absoluteFill} /> </Link> <Animated.View entering={SlideInDown} style={{ width: '90%', height: '80%', alignItems: 'center', justifyContent: 'center', backgroundColor: 'white', }} > <Text style={{ fontWeight: 'bold', marginBottom: 10 }}>模态框屏幕</Text> <Link href="/"> <Text>← 返回</Text> </Link> </Animated.View> </Animated.View> ); }
你可以根据需要自定义模态框的外观。