Web 模态框
编辑页面
了解如何使用 Expo Router 在你的 web 应用中实现并自定义模态框的行为。
For the complete documentation index, see llms.txt. Use this 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 自定义 modal 样式
注意:webModalStyle属性仅适用于 web 平台。在移动端,modal 会自动适配为适合触控交互的 sheet 行为。
你可以使用 webModalStyle 自定义 web 端 modal 的尺寸和外观。它提供了以下属性以便进一步定制:
| 属性 | 类型 | 描述 | 默认值 |
|---|---|---|---|
width | number string | 覆盖 modal 的宽度(px 或百分比)。仅适用于桌面端的 web 平台。 | 83vw |
height | number string | 覆盖 modal 的高度(px 或百分比)。仅适用于桌面端的 web 平台。 | 79vh |
minHeight | number string | 桌面端 modal 的最小高度(px 或百分比)。覆盖默认的 iOS 26 尺寸。 | min(586px, 79vh) |
minWidth | number string | 桌面端 modal 的最小宽度(px 或百分比)。覆盖默认的 iOS 26 尺寸。 | min(936px, 83vw) |
border | string | 覆盖桌面端 modal 的边框(任何有效的 CSS border 值,例如 '1px solid #ccc' 或 'none') | 无 |
overlayBackground | string | 覆盖遮罩层背景颜色(任何有效的 CSS 颜色或 rgba/hsla 值)。 | 半透明黑色 |
shadow | string | 覆盖 modal 的阴影滤镜(任何有效的 CSS filter 值,例如 'drop-shadow(0 4px 8px rgba(0,0,0,0.1))' 或 'none') | 投影阴影滤镜 |
自定义 CSS 属性
Expo Router 使用自定义 CSS 属性来设置 modal 样式,你可以通过 webModalStyle 全局覆盖这些属性。这些变量可以对 modal 的外观进行细粒度控制。
宽度和高度尺寸变量
/* 默认 modal 宽度(桌面端为 83vw,遵循 iOS 26 规范) */ --expo-router-modal-width: 83vw; /* 最大 modal 宽度(最大 936px,默认 83vw,遵循 iOS 26) */ --expo-router-modal-max-width: min(936px, 83vw); /* 最小 modal 宽度(默认 auto) */ --expo-router-modal-min-width: auto; /* 默认 modal 高度(79vh,遵循 iOS 26 规范) */ --expo-router-modal-height: 79vh; /* 最小 modal 高度(最大 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))
常见示例
全屏 modal 示例
如果要为覆盖最大空间的内容创建一个全屏 modal,可以在 modal 路由的 Stack.Screen 选项中使用 webModalStyle 属性:
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> ); }
以上示例的效果如下:
当你在移动设备上运行 web 应用时,如果你想避免显示全屏 modal,可以将 sheetAllowedDetents 设置为 fitToContents 或自定义值:
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> ); }
在移动设备上,modal 会以 sheet 的形式显示:
紧凑型 modal 示例
对于较小的交互,你可以创建一个适配内容的紧凑型 modal:
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> ); }
以上示例的效果如下:
透明 modal 示例
当你希望显示一个遮罩层并保留底层屏幕的视觉上下文时,可以将 presentation 选项设置为 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> ); }
以上示例的效果如下:
圆角示例
你可以使用 sheetCornerRadius 来自定义圆角:
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> ); }
以上示例的效果如下:
自定义 detents 示例
你可以使用 sheetAllowedDetents 来定义 modal 可停靠的高度:
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> ); }
以上示例的效果如下:
全局 CSS 自定义
对于你的 web 应用,如果项目中使用了 global CSS 文件,也可以覆盖宽度、高度、边框和遮罩层变量。
你可以在全局 CSS 文件中使用 --expo-router-* 变量添加自定义值:
/* 全局覆盖默认 modal 样式 */ :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); }
自定义 modal 路由实现
上方视频演示了一个显示在网页主内容之上的模态窗口。背景会变暗以将注意力吸引到模态窗口上,模态窗口中包含用户所需的信息。这是网页模态窗口的典型行为,用户可以与模态窗口交互,或将其关闭以返回主页面。
你可以通过使用 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', /* @info(可选)将 <CODE>animation</CODE> 设置为 <CODE>fade</CODE>。*/ 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> ); }
你可以根据需要自定义模态框的外观。