Portal System
The MeoNode UI portal system manages overlays — modals, drawers, tooltips, sheets — through a stack-based API with imperative control, automatic data sync, and full type safety.
Architecture
Three pieces work together:
PortalProvider— context provider that owns the portal stackPortalHost— the render target where active portals are mountedusePortal— the hook used inside components to open, update, and close portals
Setup
Wrap your app (or the relevant subtree) with PortalProvider and place a PortalHost where overlays should render — usually at the end of the layout so they sit above everything else:
import { PortalProvider, PortalHost } from '@meonode/ui' const Layout = ({ children }) => PortalProvider({ children: [ children, PortalHost(), // rendered last so portals stack on top ], })
Opening a portal
Define the portal content as a regular component, then open it from anywhere inside the provider:
import { usePortal, Button, Div, Text, type PortalLayerProps } from '@meonode/ui' interface ModalData { name: string } const HelloModal = ({ data, close }: PortalLayerProps<ModalData>) => Div({ padding: 24, backgroundColor: 'theme.base', children: [ Text(`Hello, ${data.name}!`), Button('Close', { onClick: close }), ], }) const App = () => { const portal = usePortal() return Button('Open Modal', { onClick: () => portal.open(HelloModal, { name: 'World' }), }) }
The portal content receives data (whatever you passed to open) and close (a function that dismisses just this layer).
Hook API
| Method | Purpose |
|---|---|
portal.open(Component, data?) | Opens a new portal layer. Returns a PortalHandle. |
portal.close() | Closes the most recently opened layer from this hook instance. |
portal.updateData(next) | Pushes new data to the most recently opened layer. |
Auto-sync
Pass data to usePortal() itself and any portal opened by that hook stays in sync — when the parent re-renders with new state, the portal receives the updated data automatically. No manual updateData calls needed.
import { useState } from 'react' import { usePortal, Button, Div, Text, type PortalLayerProps } from '@meonode/ui' interface CounterData { count: number setCount: React.Dispatch<React.SetStateAction<number>> } const CounterModal = ({ data, close }: PortalLayerProps<CounterData>) => Div({ padding: 24, backgroundColor: 'theme.base', children: [ Text(`Current count: ${data.count}`), Button('Increment', { onClick: () => data.setCount(c => c + 1) }), Button('Close', { onClick: close }), ], }) const Counter = () => { const [count, setCount] = useState(0) const portal = usePortal({ count, setCount }) // auto-sync enabled return Button('Open Counter', { onClick: () => portal.open(CounterModal), }) }
For full type safety, pass a generic to usePortal:
const portal = usePortal<CounterData>({ count, setCount })
Data Channels
For high-frequency updates (drag handles, animation values, scrubbing) you don't want to trigger a parent re-render every time. createDataChannel gives you a ref-based pub/sub primitive that updates subscribers without the React render cycle:
import { createDataChannel, useDataChannel, Text } from '@meonode/ui' const countChannel = createDataChannel(0) const Counter = () => { const count = useDataChannel(countChannel) return Text(`Count: ${count}`) } // Update from anywhere — even outside the React tree countChannel.set(countChannel.get() + 1)
channel.set takes the next value (not an updater function). Use channel.get() if you need the current value to compute the next one.
Nested Portals
Portals can open more portals. The system tracks the stack and close always refers to the layer the hook opened.
import { usePortal, Button, Div } from '@meonode/ui' const Inner = ({ close }) => Div({ padding: 24, children: [Button('Close inner', { onClick: close })], }) const Outer = ({ close }) => { const portal = usePortal() return Div({ padding: 24, children: [ Button('Open inner', { onClick: () => portal.open(Inner) }), Button('Close outer', { onClick: close }), ], }) }
Layers stack visually in mount order; closing one only dismisses that layer.
Next Steps
- Rules & Patterns — Conventions, gotchas, and idiomatic MeoNode patterns
- Framework Integration — Next.js, Vite, Remix
- FAQ — Common patterns and edge cases
- Release Notes — Changelog and upgrade guides
On this page
- Portal System