You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte/documentation/docs/03-template-syntax/20-portal.md

6.0 KiB

title: {#portal ...} / {@portal ...} tags: template-portal
<!--- copy: false  --->
{#portal key}...{/portal}
<!--- copy: false  --->
{@portal key}

Portals allow you to render markup and components into a different location in the DOM, rather than where they are defined in the template. Use cases include:

  • Declaring UI placeholders in a common parent layout and filling them declaratively inside page components
  • Portaling content to a DOM element that is outside your app and therefore outside Svelte's control
  • Creating UI elements like modals, tooltips, notifications, and dropdowns that need to break out of their parent container's styles (though in many such cases you should prefer HTML-native solutions such as the <dialog> element or the popover attribute)

A portal is declared using the {#portal ...} block:

{#portal portalKeyOrDomElement}
	contents that go elsewhere
{/portal}

Svelte supports portaling to either a declared {@portal ...} outlet using a key, or directly to a DOM element.

If the target DOM element doesn't exist or no {@portal ...} tag renders its key, the {#portal ...} block will not render its contents.

Portals with keys

To link a {#portal} block to a destination elsewhere in your markup, use any non-nullish value as a portal key. This is the recommended approach as it supports Server-Side Rendering (SSR).

<!--- file: App.svelte --->
<main>
	<h1>Home Page</h1>

	{#portal 'footer'}
		<p>This content is portaled to the footer</p>
	{/portal}
</main>

<footer>
	{@portal 'footer'}
</footer>

The content inside the {#portal 'footer'} block is moved and rendered at the exact position of the {@portal 'footer'} tag.

Ordering and scope

The {#portal ...} block and its corresponding {@portal ...} outlet do not need to be declared in any specific order in the template, and they can be inside different components.

If you need a key that cannot collide with another string, use a regular JavaScript object or symbol and pass it to child components as a prop, retrieve it from context, or export it to share it across your application.

Here's an example where two pages portal content to a common layout:

<!--- file: App.svelte --->
<script module>
	import { createContext } from 'svelte';

	const [getPortalKey, setPortalKey] = createContext();
	export { getPortalKey };
</script>

<script>
	import PageA from './PageA.svelte';
	import PageB from './PageB.svelte';

	const portalKey = Symbol('portal');
	setPortalKey(portalKey);

	let Page = $state(PageA);
</script>

<div>
	{@portal portalKey}
</div>

<button onclick={() => Page = Page === PageA ? PageB : PageA}>switch page</button>

<Page />
<!--- file: PageA.svelte --->
<script>
	import { getPortalKey } from './App.svelte';
</script>

{#portal getPortalKey()}
	on page A
{/portal}

<p>Contents of page A</p>
<!--- file: PageB.svelte --->
<script>
	import { getPortalKey } from './App.svelte';
</script>

{#portal getPortalKey()}
	on page B
{/portal}

<p>Contents of page B</p>

Portals with DOM elements

Alternatively, you can portal content directly into a DOM element by passing the element reference as the target of the {#portal ...} block:

<!--- file: App.svelte --->
<script>
	let target = $state(null);
</script>

<div class="container" bind:this={target}>
	<p>Existing static content</p>
</div>

{#portal target}
	<p>This content is appended inside the container div.</p>
{/portal}

When targeting a DOM element, Svelte will insert the portaled contents directly inside that element.

[!NOTE] Portals targeting DOM elements are client-only. Because DOM elements are not available during server-side rendering, these portal blocks are skipped on the server and will only render once the component mounts in the browser.

Portal behavior

If multiple {#portal ...} blocks target the same {@portal ...} outlet, they will appear in the order they are created within the outlet.

<!--- file: App.svelte --->
{#portal 'target'}
	a
{/portal}

{#portal 'target'}
	b
{/portal}

<!-- shows "ab" -->
{@portal 'target'}

DOM events fired within a {#portal ...} block bubble up the DOM tree, not the Svelte component tree.

<!--- file: App.svelte --->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div onclick={() => console.log('i will not be reached')}>
	{#portal 'target'}
		<button>click me</button>
	{/portal}
</div>

<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div onclick={() => console.log('i will fire')}>
	{@portal 'target'}
</div>

When retrieving something from Svelte's context, the context is relative to the {#portal ...} location, not the {@portal ...} location:

<!--- file: App.svelte --->
<script module>
	import { createContext } from 'svelte';

	const [get, set] = createContext();
	export { get, set };
</script>

<script>
	import Child from './Child.svelte';

	set('value', 'foo');
</script>

{@portal 'portal'}
<!--- file: Child.svelte --->
<script>
	import { set } from './App.svelte';
	import PortalContent from './PortalContent.svelte';

	set('value', 'bar');
</script>

{#portal 'portal'}
	<PortalContent />
{/portal}	
<!--- file: PortalContent.svelte --->
<script>
	import { get } from './App.svelte';

	const value = get();
</script>

<!-- this will show 'bar', not 'foo' -->
{value}