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 '@rootnative/components'
import { ThemeProvider } from '@rootnative/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 '@rootnative/components'
import { ThemeProvider } from '@rootnative/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 into that layer, sorted by priority.
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.
Portal entries are held outside React state, and each host subscribes only to its own entries. Opening or closing an overlay therefore re-renders the overlay layer, not the app tree underneath the host.
Stacking order
Portals in the same host stack by ascending priority. Ties fall back to mount order — open Dialog A then Dialog B at the same priority and B sits above A.
PORTAL_LAYERS is the z-order contract RootNative's own overlay components use:
| Layer | Value | Used by |
|---|---|---|
sheet | 100 | Bottom sheets and side sheets, plus their scrim |
dialog | 200 | Dialogs and their scrim |
snackbar | 300 | Snackbars — stay visible over an open dialog |
menu | 400 | Menus and dropdowns, which can open from any surface below |
tooltip | 500 | Tooltips — topmost, they can describe a control in any layer |
The 100-point gaps are deliberate: slot a custom overlay between two layers with PORTAL_LAYERS.dialog + 1 without renumbering anything.
import { Portal, PORTAL_LAYERS } from '@rootnative/components'
<Portal priority={PORTAL_LAYERS.dialog}>
<MyDialog />
</Portal>
A <Portal> with no priority defaults to 0, below every named layer.
Named hosts
<Portal hostName="…"> targets a <PortalHost name="…"> elsewhere in the tree instead of the root overlay. Use this to scope overlays to a screen or a nested navigator — the named host renders its portals where it sits, so they are clipped and positioned by that subtree rather than by the app root.
<PortalHost>
<App />
<View style={styles.sheetArea}>
<PortalHost name="sheet-area" />
</View>
</PortalHost>
Two rules follow from that:
priorityorders portals within one host only. Stacking between hosts follows tree position. Anything that must participate in the app-wide z-order belongs in the default host with aPORTAL_LAYERSpriority — that is what RootNative's own Dialog, Snackbar, Menu, Tooltip, and BottomSheet do.- An unmatched
hostNamefalls back to the default host rather than dropping the content. If the named host mounts later, the portal moves to it; if it unmounts, the portal moves back.
The root host's name is exported as DEFAULT_PORTAL_HOST (the string 'default'), so code that needs to target it explicitly — or compare against it — can import the constant instead of repeating the literal:
import { DEFAULT_PORTAL_HOST } from '@rootnative/components/portal'
<Portal hostName={DEFAULT_PORTAL_HOST}>{overlay}</Portal>
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
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
children | ReactNode | - | Yes | Content teleported into the host's overlay layer. |
hostName | string | default | No | Name of the host to render into. Falls back to the default host when no host with this name is mounted. |
priority | number | 0 | No | Stack order within the host — higher renders above lower, ties broken by mount order. Use the `PORTAL_LAYERS` constants for the layers RootNative itself defines. |
PortalHost Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
children | ReactNode | - | No | Content rendered inside the host. Optional on a named (slot) host. |
name | string | - | No | Registers this host under a name that `<Portal hostName="…">` can target. A named host must sit inside a root `<PortalHost>` — it renders its portals where it appears in the tree instead of in the root overlay, which is the point of naming one (scoping overlays to a screen or a nested navigator). Cross-host stacking therefore follows tree position, not `priority`; `priority` only orders portals inside the same host. Anything that has to participate in the app-wide z-order belongs in the default host with a `priority` from `PORTAL_LAYERS`. |
style | StyleProp<ViewStyle> | - | No | Style applied to the root container on a root host, or to the overlay container on a named host (which absolute-fills its parent by default). |
onKeyDown | (event: { nativeEvent: { key?: string; }; }) => void | - | No | - |