Portal
Send items to other areas of the tree
Portal is included in @hanzo/gui and is used by Sheet,
Dialog, Popover,
Select, and Toast. See Z-Index & Stacking for how overlay layering works.
Native Portal Setup (Recommended)
On web, React's built-in createPortal preserves context automatically. On
native, the default portal implementation doesn't preserve React context.
Hanzo GUI automatically re-propagates its own contexts (theme, configuration), but
your custom contexts like navigation or app state won't be available inside
portaled content. The re-propagation also adds some overhead.
We recommend using react-native-teleport to solve this. It uses React Native's native portal API to preserve context automatically.
Step 1: Install react-native-teleport
npm install react-native-teleportStep 2: Import the setup module
In your app's entry file (index.js or App.tsx), before any Hanzo GUI imports:
import '@hanzogui/native/setup-teleport'That's it! All portal-using components will now preserve context automatically on native. Without native portals, your custom context from parent components won't be available inside portaled content:
const MyContext = createContext('default')
function App() {
return (
<MyContext.Provider value="from-provider">
<Sheet modal open>
<Sheet.Frame>
{/* Without native portal: you'd not have context here on native */}
<MyConsumer />
</Sheet.Frame>
</Sheet>
</MyContext.Provider>
)
}Alternative Approaches
If you can't use react-native-teleport, there are other ways to handle context in portals:
Component Scoping
For Dialog, Popover, and Tooltip, use the scope prop to mount a single
instance at your app root. This avoids portals entirely on native:
// _layout.tsx - mount once at root with all your providers
<GuiProvider>
<Tooltip scope="global">
<Tooltip.Content>
<Tooltip.Arrow />
<Paragraph>{/* label set by trigger */}</Paragraph>
</Tooltip.Content>
{/* rest of your app */}
<Slot />
</Tooltip>
</GuiProvider>
// anywhere in your app - just the trigger
<Tooltip.Trigger scope="global" aria-label="Settings">
<Button icon={Settings} />
</Tooltip.Trigger>This pattern is also a performance win for lists or tables with many interactive elements.
Manual Context Re-propagation
Wrap portal children with the necessary providers:
const theme = useTheme()
const myValue = useContext(MyContext)
<Sheet>
<Sheet.Frame>
<ThemeProvider theme={theme}>
<MyContext.Provider value={myValue}>
{/* content that needs context */}
</MyContext.Provider>
</ThemeProvider>
</Sheet.Frame>
</Sheet>This is more verbose and error-prone as you need to remember to re-propagate every context you use.
API Reference
Portal
| Prop | Type | Default | Required |
|---|---|---|---|
| zIndex | number | - | - |
| stackZIndex | boolean | number | 'global' | - | - |
| passThrough | boolean | - | - |
This automatic stacking is already enabled by default in Dialog, Popover, Sheet, and other overlay components. If you open a Popover from within a Dialog, the Popover will automatically have a higher z-index than the Dialog without any configuration needed.
Technical Details
react-native-teleport
uses ReactNativeFabricUIManager.createPortal (Fabric) or
UIManager.createPortal (Paper) to create true native portals that preserve the
React fiber tree.
The default portal implementation, by contrast, uses a JS-based approach with context providers and a reducer to manage portal state. While compatible with older RN versions, it breaks React context because it re-renders content in a separate provider tree.
Hanzo GUI includes a needsPortalRepropagation() helper that returns true when
using the default portal implementation and false when using native portals,
so library authors can conditionally re-propagate context only when needed.
Last updated on