Portals#
<Portal> renders its children above the rest of the UI, outside their normal place in the tree. Reach for it for modals, tooltips, dropdowns, and overlays.
import { Portal, View } from "onejs-react"
<Portal>
<View style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 }} />
</Portal>Zero setup: children mount into a shared overlay layer that OneJS keeps as the last child of __root, so they always paint on top.
Why portals#
UI Toolkit has no z-index:
- Paint order follows the hierarchy. Later siblings draw on top.
overflow: hiddenclips children.
<Portal> lifts it out to the shared overlay layer, escaping both.
Modal#
import { useState } from "react"
import { Portal, View, Button, Label } from "onejs-react"
function Modal({ onClose, children }) {
return (
<Portal>
<View
style={{
position: "absolute", top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: "rgba(0,0,0,0.6)",
alignItems: "center", justifyContent: "center",
}}
onClick={onClose}
>
<View
style={{ padding: 24, backgroundColor: "#1e1e1e", borderRadius: 12 }}
onClick={(e) => e.stopPropagation()}
>
{children}
</View>
</View>
</Portal>
)
}
function App() {
const [open, setOpen] = useState(false)
return (
<View style={{ padding: 20 }}>
<Button text="Open" onClick={() => setOpen(true)} />
{open && (
<Modal onClose={() => setOpen(false)}>
<Label text="Hello from a portal" />
<Button text="Close" onClick={() => setOpen(false)} />
</Modal>
)}
</View>
)
}Full-screen backdrop, centered card, on top of everything. open && ... mounts and unmounts the modal. The overlay layer ignores picking when empty, so a closed modal never blocks the app.
Event propagation#
Events bubble along where an element is mounted, not the React parent tree. Inside the portal, bubbling and stopPropagation() work as normal (that is how the card swallows the backdrop click above). But a portal's events do not reach handlers on its React-parent component. This differs from React DOM, where portal events follow the React tree. Keep handlers inside the portal subtree.
Tooltips#
Same recipe: wrap in <Portal>, position absolutely from the trigger's worldBound (read via a ref).
const r = triggerRef.current.worldBound // { x, y, width, height }
setPos({ left: r.x, top: r.y + r.height + 4 })createPortal#
<Portal> is built on createPortal(children, container, key?), the OneJS equivalent of React DOM's createPortal. Use it directly when you need a specific target:
import { createPortal } from "onejs-react"
createPortal(children, someElement)With a custom target you own draw order. The shared overlay layer is what keeps <Portal> on top, so prefer <Portal> for overlays.
Import fromonejs-react, notreact-dom. React DOM targets the browser DOM and will not work with UI Toolkit.