Skip to main content

Portal

Render content into a shared overlay layer that sits above the rest of the app. Portal is the foundation primitive for dialogs, snackbars, menus, tooltips, and bottom sheets — anything that needs to escape its parent's layout and z-order.

Setup

Wrap your app root with PortalHost once. Every <Portal> rendered anywhere in the tree below will mount into this host's overlay layer.

import { PortalHost, Typography } from '@onlynative/components'
import { ThemeProvider } from '@onlynative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider>
<PortalHost>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Typography variant="bodyLarge">App content lives here.</Typography>
</View>
</PortalHost>
</ThemeProvider>
</SafeAreaProvider>
)
}

Usage

import { Button, Portal, PortalHost, Typography } from '@onlynative/components'
import { ThemeProvider } from '@onlynative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { useState } from 'react'
import { Pressable, View } from 'react-native'

function Screen() {
const [open, setOpen] = useState(false)
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button variant="filled" onPress={() => setOpen(true)}>Open overlay</Button>
{open ? (
<Portal>
<Pressable
onPress={() => setOpen(false)}
style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.4)', alignItems: 'center', justifyContent: 'center' }}
>
<View style={{ backgroundColor: 'white', padding: 24, borderRadius: 12 }}>
<Typography variant="titleMedium">Hello from a Portal</Typography>
<Typography variant="bodyMedium">Tap outside to close.</Typography>
</View>
</Pressable>
</Portal>
) : null}
</View>
)
}

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider>
<PortalHost>
<Screen />
</PortalHost>
</ThemeProvider>
</SafeAreaProvider>
)
}

The Portal component itself renders nothing inline — its children appear inside the host's overlay layer instead. Mount and unmount as you would any normal React component.

How it works

PortalHost wraps its children in a root View and adds an absolutely-positioned overlay layer on top. Each <Portal> registers its children with the host via context; the host renders them in mount order, so the most recently mounted Portal appears on top.

The overlay layer uses pointerEvents="box-none", so empty regions of the overlay pass touches through to the content below. Each Portal entry is responsible for its own positioning and scrim — typically with position: 'absolute' and a full-bleed touch-capturing View.

Stacking order

Portals render in mount order. If you open Dialog A then Dialog B, Dialog B sits above Dialog A. Closing B brings A back to the top.

Without a host

If <Portal> is rendered outside a <PortalHost>, it logs a development error and falls back to inline rendering so your screen still works. Add a PortalHost at the app root to silence the warning and get the overlay behavior.

Portal Props

This component has no custom props.

PortalHost Props

PropTypeDefaultRequiredDescription
styleStyleProp<ViewStyle>-NoStyle applied to the root container that wraps host children and the overlay layer.