Skip to content

Teleport

The data-teleport attribute renders element content at a different DOM location while maintaining full reactivity. This is essential for modals, tooltips, dropdown menus, and any UI that needs to escape its parent's CSS context (overflow, z-index, positioning).

Basic Usage

data-teleport (Hyper)

Wrap content in a <template> element with data-teleport pointing to a CSS selector:

blade
<div data-signals="{modalOpen: false}">
    <button data-on:click="$modalOpen = true">Open Modal</button>

    <template data-teleport="body">
        <div data-show="$modalOpen" class="fixed inset-0 bg-black/50 flex items-center justify-center">
            <div class="bg-white p-6 rounded-lg">
                <h2>Modal Title</h2>
                <p>This modal is rendered at the body element.</p>
                <button data-on:click="$modalOpen = false">Close</button>
            </div>
        </div>
    </template>
</div>

The modal content is physically moved to <body> but still reacts to the $modalOpen signal from its original location.

Why use teleport?

Without teleport, modals inside containers with overflow: hidden or specific z-index values may be clipped or layered incorrectly. Teleporting to body ensures proper stacking.

Placement Modifiers

__prepend

Insert content before the target element:

blade
<template data-teleport__prepend="#notifications">
    <div class="alert">New notification</div>
</template>

<div id="notifications">
    <!-- Teleported content appears before existing content -->
    <div>Existing notification</div>
</div>

__append

Insert content after the target element:

blade
<template data-teleport__append="#sidebar">
    <div class="widget">Additional widget</div>
</template>

<div id="sidebar">
    <div>Main sidebar content</div>
    <!-- Teleported content appears after -->
</div>

Default behavior (no modifier): Replaces target's innerHTML with teleported content.

Selectors

data-teleport accepts any valid CSS selector:

blade
<!-- By ID -->
<template data-teleport="#modal-root">...</template>

<!-- By class -->
<template data-teleport=".portal-container">...</template>

<!-- Complex selectors -->
<template data-teleport="[data-portal='notifications']">...</template>
<template data-teleport="#app > .layout > footer">...</template>

TIP

For predictability, prefer ID selectors (#modal-root) over class or complex selectors. Create dedicated portal targets in your layout.

Reactivity

Teleported content maintains full reactivity with signals and bindings:

blade
<div data-signals="{count: 0, message: ''}">
    <input data-bind="message" placeholder="Type a message" />
    <button data-on:click="$count++">Increment</button>

    <template data-teleport="#display-area">
        <div class="teleported-content">
            <p>Count: <span data-text="$count"></span></p>
            <p>Message: <span data-text="$message"></span></p>
            <button data-on:click="$count = 0">Reset</button>
        </div>
    </template>
</div>

<div id="display-area">
    <!-- Teleported content here reacts to signals above -->
</div>

Event Forwarding

Events from teleported content bubble back to the original template location:

blade
<div data-signals="{open: false}">
    <template data-teleport="body" data-on:click="$open = false">
        <div data-show="$open" class="modal-backdrop">
            <div class="modal" data-on:click__stop>
                <!-- Click inside modal doesn't close it (stopPropagation) -->
                <p>Modal content</p>
            </div>
        </div>
    </template>
</div>

The data-on:click on the template catches clicks that bubble up from the teleported content.

Common Patterns

blade
<div data-signals="{showModal: false}">
    <button data-on:click="$showModal = true" class="btn">
        Open Modal
    </button>

    <template data-teleport="body">
        <div data-show="$showModal"
             class="fixed inset-0 z-50"
             data-transition:enter="transition-opacity duration-200"
             data-transition:enter-start="opacity-0"
             data-transition:enter-end="opacity-100"
             data-transition:leave="transition-opacity duration-150"
             data-transition:leave-start="opacity-100"
             data-transition:leave-end="opacity-0">

            <!-- Backdrop -->
            <div class="absolute inset-0 bg-black/50"
                 data-on:click="$showModal = false"></div>

            <!-- Modal content -->
            <div class="relative z-10 flex items-center justify-center min-h-screen p-4">
                <div class="bg-white rounded-lg shadow-xl max-w-md w-full p-6"
                     data-transition__scale__95>
                    <h2 class="text-xl font-bold">Modal Title</h2>
                    <p class="mt-2 text-gray-600">Modal content goes here.</p>
                    <div class="mt-4 flex justify-end gap-2">
                        <button data-on:click="$showModal = false" class="btn-secondary">
                            Cancel
                        </button>
                        <button data-on:click="handleConfirm(); $showModal = false" class="btn-primary">
                            Confirm
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </template>
</div>
blade
<div data-signals="{dropdownOpen: false}" class="relative">
    <button data-on:click="$dropdownOpen = !$dropdownOpen">
        Menu
    </button>

    <template data-teleport__append="body">
        <div data-show="$dropdownOpen"
             data-on:click__outside="$dropdownOpen = false"
             class="fixed bg-white shadow-lg rounded-md py-2 z-50"
             data-transition>
            <a href="/profile" class="block px-4 py-2 hover:bg-gray-100">Profile</a>
            <a href="/settings" class="block px-4 py-2 hover:bg-gray-100">Settings</a>
            <button data-on:click="@postx('/logout')" class="block w-full text-left px-4 py-2 hover:bg-gray-100">
                Logout
            </button>
        </div>
    </template>
</div>

Positioning

Teleported dropdowns need positioning logic. Consider using CSS position: fixed with JavaScript to calculate coordinates, or use a positioning library.

Toast Notifications

blade
<div data-signals="{toasts: []}">
    <button data-on:click="$toasts.push({id: Date.now(), message: 'Action completed!'})">
        Show Toast
    </button>

    <template data-teleport="#toast-container">
        <template data-for="toast in $toasts" data-for__key="id">
            <div class="toast bg-green-500 text-white px-4 py-2 rounded mb-2"
                 data-transition
                 data-on:click="$toasts = $toasts.filter(t => t.id !== toast.id)">
                <span data-text="toast.message"></span>
            </div>
        </template>
    </template>
</div>

<!-- In your layout -->
<div id="toast-container" class="fixed top-4 right-4 z-50"></div>

Multiple Teleports to Same Target

Multiple teleports can target the same container:

blade
<template data-teleport__append="#notifications">
    <div>First notification</div>
</template>

<template data-teleport__append="#notifications">
    <div>Second notification</div>
</template>

<div id="notifications">
    <!-- Both teleported items appear here -->
</div>

Nested Teleports

Teleports can be nested (modal within modal):

blade
<div data-signals="{outerModal: false, innerModal: false}">
    <button data-on:click="$outerModal = true">Open Outer Modal</button>

    <template data-teleport="body">
        <div data-show="$outerModal" class="modal outer">
            <h2>Outer Modal</h2>
            <button data-on:click="$innerModal = true">Open Inner Modal</button>
            <button data-on:click="$outerModal = false">Close</button>

            <template data-teleport="body">
                <div data-show="$innerModal" class="modal inner" style="z-index: 60">
                    <h2>Inner Modal</h2>
                    <button data-on:click="$innerModal = false">Close Inner</button>
                </div>
            </template>
        </div>
    </template>
</div>

Layout Setup

Create dedicated portal targets in your layout for predictable teleport destinations:

blade
<!DOCTYPE html>
<html>
<head>
    <title>My App</title>
    @hyper
</head>
<body>
    <div id="app">
        @yield('content')
    </div>

    <!-- Portal targets -->
    <div id="modal-root"></div>
    <div id="toast-container" class="fixed top-4 right-4 z-50"></div>
    <div id="dropdown-container"></div>
</body>
</html>

Cleanup

When the original <template> element is removed from the DOM, its teleported content is automatically cleaned up. This ensures no orphaned elements remain.

Learn More