Teleport

Renders children under a different DOM node than their logical position in the element tree — the building block for modals, dropdowns, and tooltips.

Teleport

Demo
Code
from webcompy.app import WebComPyApp
from webcompy.components import ComponentContext, define_component
from webcompy.elements import Teleport, html, switch
from webcompy.signal import use_state


def _modal(close_handler):
    return Teleport(
        {"to": "body"},
        html.DIV(
            {"class": "demo-modal-backdrop"},
            html.DIV(
                {"class": "demo-modal"},
                html.H2({}, "Teleported Modal"),
                html.P({}, "This modal is rendered as a direct child of <body> via Teleport."),
                html.BUTTON({"id": "close-modal", "@click": close_handler}, "Close"),
            ),
        ),
    )


def _dropdown():
    return Teleport(
        {"to": "body"},
        html.UL(
            {"class": "demo-dropdown"},
            html.LI({"class": "demo-dropdown-item"}, "Dropdown Action 1"),
            html.LI({"class": "demo-dropdown-item"}, "Dropdown Action 2"),
            html.LI({"class": "demo-dropdown-item"}, "Dropdown Action 3"),
        ),
    )


@define_component("teleport-demo-app")
def TeleportDemo(_: ComponentContext[None]):
    modal_open = use_state(lambda: False)
    dropdown_open = use_state(lambda: False)

    def _toggle_modal(_ev):
        modal_open.value = not modal_open.value

    def _toggle_dropdown(_ev):
        dropdown_open.value = not dropdown_open.value

    return html.DIV(
        {"class": "teleport-demo"},
        html.H1({}, "Teleport Demo"),
        html.P({}, "The modal and the dropdown below are rendered under <body> via Teleport."),
        html.DIV(
            {"class": "demo-controls"},
            html.BUTTON({"id": "open-modal", "@click": _toggle_modal}, "Open Modal"),
            html.BUTTON({"id": "toggle-dropdown", "@click": _toggle_dropdown}, "Toggle Dropdown"),
        ),
        switch({"case": modal_open, "generator": lambda: _modal(_toggle_modal)}, default=None),
        switch({"case": dropdown_open, "generator": _dropdown}, default=None),
    )


TeleportDemo.scoped_style = {
    ".teleport-demo": {
        "font-family": "sans-serif",
        "padding": "1rem",
    },
    ".demo-controls": {
        "display": "flex",
        "gap": "0.5rem",
        "margin-top": "1rem",
    },
    ".demo-controls button": {
        "padding": "0.5rem 1rem",
        "cursor": "pointer",
    },
    ".demo-modal-backdrop": {
        "position": "fixed",
        "inset": "0",
        "background": "rgba(0, 0, 0, 0.4)",
        "display": "flex",
        "align-items": "center",
        "justify-content": "center",
        "z-index": "1000",
    },
    ".demo-modal": {
        "background": "white",
        "color": "black",
        "padding": "1.5rem",
        "border-radius": "0.5rem",
        "min-width": "20rem",
    },
    ".demo-dropdown": {
        "position": "fixed",
        "top": "6rem",
        "left": "1rem",
        "z-index": "1000",
        "background": "white",
        "color": "black",
        "border": "1px solid #ccc",
        "list-style": "none",
        "padding": "0.5rem",
        "margin": "0",
        "min-width": "12rem",
    },
    ".demo-dropdown-item": {
        "padding": "0.25rem 0.5rem",
    },
}

app = WebComPyApp(root_component=TeleportDemo)
app.run()

Using Teleport

Wrap a subtree with Teleport and give it a target selector: Teleport({"to": "body"}, *children). The children are mounted under the resolved target node instead of their logical parent.

Target selection

The 'to' selector MUST address a stable node that is not produced or removed by the application's own rendering — typically 'body' or a static element in the host page. If the target is removed externally, the teleported content is detached with it; the framework does not re-resolve or recover the target.

Server-side rendering

During SSR and static generation, Teleport renders its children into the resolved target by default, so the content is present in the served HTML for crawlers and no-JS clients. Pass "ssr": False in the props to opt out and keep the anchor-only output; the browser then mounts the children during hydration as before.