Web 模态窗口
了解如何使用 Expo Router 在你的 Web 应用中实现和自定义模态框的行为。
重要 Web 模态处于测试阶段,并在 SDK 54 及更高版本中可用。要使用此功能,你必须在项目中设置
EXPO_UNSTABLE_WEB_MODAL=1环境变量。
现代网页应用需要一种灵活的模态体验,以适应不同的内容尺寸和用户交互。Expo Router 为现代网页体验提供了各种模态呈现模式。这些模式利用 presentation 配合 modal、formSheet、transparentModal 或 containedTransparentModal 来根据不同的屏幕宽度呈现模态,并使用 webModalStyle 提供可自定义的样式属性。
🌐 Modern web apps require a flexible modal experience that adapts to different content sizes and user interactions. Expo Router provides various modal presentation patterns for modern web experiences. These patterns leverage presentation with modal, formSheet, transparentModal, or containedTransparentModal to present either a modal based on different screen widths, and provide customizable styling props using webModalStyle.
开始使用
🌐 Get started
信息 要使用新的网页模态功能,你必须在开发和导出构建中设置
EXPO_UNSTABLE_WEB_MODAL=1环境变量。你可以通过将其添加到项目根目录下的 .env 文件中,或者在命令前添加前缀来实现,例如:EXPO_UNSTABLE_WEB_MODAL=1 npx expo start。
Expo Router 中的模态框使用 Stack.Screen 组件及特定选项进行配置。这要求将模态屏幕添加到应用的 Stack 布局文件中。
🌐 Modals in Expo Router are configured using Stack.Screen component with specific options. This requires the modal screen to be added to the layout file of your app's Stack.
请考虑以下导航树,其中包括布局文件中定义的堆栈导航器、用于访问模态窗口的主屏幕以及模态窗口屏幕组件:
🌐 Consider the following navigation tree, which includes a stack navigator defined in the layout file, a home screen where the modal is accessed, and the modal screen component:
app_layout.tsxindex.tsxmodal.tsx在布局文件 (app/_layout.tsx) 中,将模态屏幕组件添加到 Stack 导航器中:
🌐 In the layout file (app/_layout.tsx), the modal screen component is added to the Stack navigator:
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', // Enables modal behavior sheetAllowedDetents: [0.5, 1], // Array of snap positions for screens that have a width less than 768px. }} /> </Stack> ); }
modal.tsx 用于显示模态框的内容:
🌐 The modal.tsx is used to display the contents of a modal:
import { Text, View } from 'react-native'; export default function Modal() { return <View style={{ flex: 1, padding: 16 }}>{/* Modal content goes here */}</View>; }
现在,要从 index.tsx 打开模态框,你可以在你的索引路由中使用 router.push('/modal'):
🌐 Now, to open the modal from index.tsx, you can use router.push('/modal') in your index route:
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}>Home Screen</Text> <Pressable onPress={() => router.push('/modal')} style={styles.button}> <Text style={styles.buttonText}>Open Modal</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', }, });
以下是上述示例的结果:
🌐 Here's the result of the above example:
锚点和嵌套堆栈
🌐 Anchors and nested stacks
在使用堆栈或嵌套堆栈导航器时,模态窗口需要正确固定,以确保导航行为的正确性,尤其是在深度链接到模态路由时。如果不固定,模态窗口背后的屏幕将被清除,导致没有导航上下文。
🌐 When working with stack or nested stack navigators, modals need to be properly anchored to ensure correct navigation behavior, especially when deep-linking to modal routes. Without anchoring, the screen behind the modal will be wiped away, leaving no navigation context.
一个_锚点_作为模态窗口的基础。在复杂的应用中,当你有嵌套堆栈时,必须为嵌套堆栈定义锚点,其值将成为堆栈的初始路由。
🌐 An anchor serves as the base for the modal. In complex apps, when you have nested stacks, the anchor must be defined for the nested stack, and its value becomes the initial route of the stack.
你可以通过从堆栈的布局文件中导出 unable_settings 来配置锚点:
🌐 You can configure an anchor by exporting unable_settings from your stack's layout file:
export const unstable_settings = { anchor: 'index', // Anchor to the index route };
在上面的示例中,anchor: 'index' 告诉 Expo 路由在显示模态窗口时应在后台保持指定的锚点路由。
🌐 In the above example, the anchor: 'index' tells the Expo Router that it should maintain the specified anchor route in the background when presenting a modal.
模态渲染样式
🌐 Modal presentation style
在你的网络应用中,模式窗口在大屏幕(例如桌面)上显示的方式与在移动设备上运行时保持表单行为之间的差异,取决于配置选项。以下是可用于配置网络模式窗口外观的选项,这些选项可以传递给 Stack.Screen 的 options 对象。
🌐 The difference between the presentation of how a modal appears in your web app on a large screen (for example, a desktop) while maintaining the sheet behavior when the web app runs on a mobile device, depends on the configuration options. The following are options available for configuring a web modal's appearance that can be passed to the options object of a Stack.Screen.
| Option | Type | Description |
|---|---|---|
presentation | 'modal', 'formSheet', 'transparentModal', 'containedTransparentModal' | Modal presentation style. On screens with width more than 768px, all styles display as a centered overlay (for example, a lightbox). On screens with width less than 768px, formSheet is used to display as a bottom sheet.When set to transparentModal, it displays as an overlay without a completely obscure background content. Detents and sheet grabber properties are not applied. This presentation is useful when building your own custom modal. Similar to transparentModal, when set to containedTransparentModal, it displays as an overlay without a completely obscure background content. Detents and other properties are not applied. This presentation is useful when building your own custom modal. |
sheetAllowedDetents | number[], 'fitToContents' | Snap positions as percentages (0.0-1.0) or automatic fitting. Only applies to screens with less than 768px width. |
sheetGrabberVisible | boolean | On iOS, shows/hides the drag handle at the top of the sheet. Not supported on Android and web. We recommend using a custom sheet header component to imitate the grabber across all platforms. |
sheetCornerRadius | number | Corner radius of the sheet in pixels. |
webModalStyle | WebModalStyle | Special prop that allows web-specific styling options for fine-tuning modal appearance. |
使用 webModalStyle 自定义模态样式
🌐 Custom modal styling with webModalStyle
信息 注意:
webModalStyle属性仅适用于网页平台。在移动端,模态框会自动调整为类似表格的行为以支持触控操作。
你可以使用 webModalStyle 来自定义网页上模态框的尺寸和外观。它提供了以下属性以供进一步自定义:
🌐 You can use webModalStyle to customize the dimensions and appearance of your modals on web. It provides the following properties for further customization:
| Property | Type | Description | Default |
|---|---|---|---|
width | number string | Override the width of the modal (px or percentage). Only applies for web platform on a desktop. | 83vw |
height | number string | Override the height of the modal (px or percentage). Only applies for web platform on a desktop. | 79vh |
minHeight | number string | Minimum height of the desktop modal (px or percentage). Overrides the default iOS 26 sizing. | min(586px, 79vh) |
minWidth | number string | Minimum width of the desktop modal (px or percentage). Overrides the default iOS 26 sizing. | min(936px, 83vw) |
border | string | Override the border of the desktop modal (any valid CSS border value, for example, '1px solid #ccc' or 'none') | None |
overlayBackground | string | Override the overlay background color (any valid CSS color or rgba/hsla value). | Semi-transparent black |
shadow | string | Override the modal shadow filter (any valid CSS filter value, for example, 'drop-shadow(0 4px 8px rgba(0,0,0,0.1))' or 'none') | Drop-shadow filter |
自定义 CSS 属性
🌐 Custom CSS properties
Expo Router 使用自定义 CSS 属性来设置模态框的样式,你可以使用 webModalStyle 全局覆盖这些属性。这些变量可以对模态框的外观进行精细控制。
🌐 Expo Router uses custom CSS properties to style modals, which you can override globally using webModalStyle. These variables provide fine-grained control over a modal's appearance.
宽度和高度大小变量
🌐 Width and height sizing variables
/* Default modal width (83vw on desktop, following iOS 26 specifications) */ --expo-router-modal-width: 83vw; /* Maximum modal width (936px max, 83vw by default, following iOS 26) */ --expo-router-modal-max-width: min(936px, 83vw); /* Minimum modal width (auto by default) */ --expo-router-modal-min-width: auto; /* Default modal height (79vh, following iOS 26 specifications) */ --expo-router-modal-height: 79vh; /* Minimum modal height (586px max, 79vh by default, following iOS 26) */ --expo-router-modal-min-height: min(586px, 79vh);
边框和叠加层样式变量
🌐 Border and overlay styling variables
/* Modal border (none by default) */ --expo-router-modal-border: none; /* Modal corner radius (24px by default, following iOS 26) */ --expo-router-modal-border-radius: 24px; /* Modal shadow filter (drop-shadow by default) */ --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)); /* Overlay background color (25% black by default) */ --expo-router-modal-overlay-background: rgba(0, 0, 0, 0.25);
webModalStyle 如何映射到 CSS 变量
🌐 How webModalStyle maps to CSS variables
当你使用 webModalStyle 覆盖任何尺寸变量时,Expo Router 会自动将这些 CSS 变量设置为你提供的值:
🌐 When you use webModalStyle to override any of the sizing variables, Expo Router automatically sets these CSS variables to the values you provide:
// This webModalStyle configuration 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))', } // ...automatically sets these CSS variables: // --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
全屏模态示例
要创建一个覆盖最大空间的全屏内容模态框,你可以在模态框路由的 Stack.Screen 选项中使用 webModalStyle 属性:
🌐 To create a full screen modal for content that covers maximum space, you can use webModalStyle property in your 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> ); }
以下是上述示例的结果:
🌐 Here's the result of the above example:
在移动设备上运行你的 web 应用时,如果你想避免显示全屏模式,可以将 sheetAllowedDetents 设置为 fitToContents 或自定义值:
🌐 When running your web app on mobile devices, you can set sheetAllowedDetents to fitToContents or a custom value if you want to avoid showing a full screen 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: '95vw', height: '95vh', border: 'none', }, sheetAllowedDetents: 'fitToContents', }} /> </Stack> ); }
在移动设备上,模态会以表单形式显示:
🌐 The modal appears as a sheet on a mobile device:
紧凑型模态示例
对于较小的交互,你可以创建一个适合其内容的紧凑型模态框:
🌐 For smaller interactions, you can create a compact modal that fits 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> ); }
以下是上述示例的结果:
🌐 Here's the result of the above example:
透明模态示例
当你想显示一个覆盖层并保持底层屏幕的视觉上下文时,可以将 presentation 选项设置为 transparentModal:
🌐 You can set the presentation option to transparentModal when you want to display an overlay that should maintain the visual context of the underlying screen:
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> ); }
以下是上述示例的结果:
🌐 Here's the result of the above example:
圆角示例
你可以使用 sheetCornerRadius 自定义角半径:
🌐 You customize the corner radius using 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> ); }
以下是上述示例的结果:
🌐 Here's the result of the above example:
自定义卡槽示例
你可以使用 sheetAllowedDetents 来定义模态框可以停留的高度:
🌐 You can use sheetAllowedDetents to define the height at which the modal can rest:
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> ); }
以下是上述示例的结果:
🌐 Here's the result of the above example:
全局 CSS 自定义
🌐 Global CSS customization
对于你的网页应用,如果你在项目中使用了全局 CSS 文件,你也可以覆盖宽度、高度、边框和覆盖变量。
🌐 For your web app, if you are using a global CSS file in your project, you can also override width, height, border, and overlay variables.
你可以在全局 CSS 文件中使用 --expo-router-* 变量添加自定义值:
🌐 You can add custom values using the --expo-router-* variables in your global CSS file:
/* Override default modal styling globally */ :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
上面的视频演示了一个模态窗口,它会出现在网页的主要内容之上。背景会变暗,以吸引用户注意模态窗口,其中包含用户需要的信息。这是网页模态窗口的典型行为,用户可以与模态窗口进行交互,也可以关闭它以返回主页面。
🌐 The video above demonstrates a modal window that appears over the main content of the web page. The background dims to draw focus to the modal, which contains information for the user. This is typical behavior for web modals, where users can interact with the modal or close it to return to the main page.
你可以通过使用 transparentModal 演示模式、对覆盖层和模态内容进行样式设置,以及利用 react-native-reanimated 来实现模态的动画展示,从而实现上述网页模态行为。
🌐 You can achieve the above web modal behavior by using the transparentModal presentation mode, styling the overlay and modal content, and utilizing react-native-reanimated to animate the modal's presentation.
修改你项目的根布局 (app/_layout.tsx) ,在模态路由中添加一个 options 对象:
🌐 Modify your project's root layout (app/_layout.tsx) to add an options object to the modal route:
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浏览器。
上述示例将 index 屏幕设置为使用 unstable_settings 的 initialRouteName。这确保了透明模态窗口始终渲染在当前屏幕之上,即使用户通过直接链接导航到模态屏幕时也是如此。
🌐 The above example sets the index screen as the initialRouteName using unstable_settings. This ensures that the transparent modal is always rendered on top of the current screen, even when users navigate to the modal screen via a direct link.
在 modal.tsx 中按如下所示设置覆盖层和模态内容的样式:
🌐 Style the overlay and modal content in modal.tsx as shown below:
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', }} > {/* Dismiss modal when pressing outside */} <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 }}>Modal Screen</Text> <Link href="/"> <Text>← Go back</Text> </Link> </Animated.View> </Animated.View> ); }
你可以根据需要自定义模态框的外观。
🌐 You can customize the modal's appearance as per your needs.