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:
<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:
<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:
<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:
<!-- 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:
<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:
<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
Modal with Overlay
<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>Dropdown Menu
<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
<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:
<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):
<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:
<!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
- Styling - Transitions - Animate teleported content
- Events - Handle events on teleported content
- Display & Binding - Use signals in teleported content
- Datastar Attributes - API reference
