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 '@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:

LayerValueUsed by
sheet100Bottom sheets and side sheets, plus their scrim
dialog200Dialogs and their scrim
snackbar300Snackbars — stay visible over an open dialog
menu400Menus and dropdowns, which can open from any surface below
tooltip500Tooltips — 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:

  • priority orders 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 a PORTAL_LAYERS priority — that is what RootNative's own Dialog, Snackbar, Menu, Tooltip, and BottomSheet do.
  • An unmatched hostName falls 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

PropTypeDefaultRequiredDescription
childrenReactNode-YesContent teleported into the host's overlay layer.
hostNamestringdefaultNoName of the host to render into. Falls back to the default host when no host with this name is mounted.
prioritynumber0NoStack 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

PropTypeDefaultRequiredDescription
childrenReactNode-NoContent rendered inside the host. Optional on a named (slot) host.
namestring-NoRegisters 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`.
styleStyleProp<ViewStyle>-NoStyle 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-