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: hidden clips children.
So an overlay nested deep in your tree gets clipped and stuck below later siblings. <Portal> lifts it out to the shared overlay layer, escaping both.
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 from onejs-react, not react-dom. React DOM targets the browser DOM and will not work with UI Toolkit.