Skip to content

Regions

Snippets

Snippets cross an island boundary as regions. Static content freezes to server HTML, a snippet that has to wake hydrates on the far side, and region.snippet() is the manual form.

On this page

A snippet is a function, and an island’s props are serialized, so on its own a snippet could never reach the inside of an island. Yet <Tabs>{@render children()}</Tabs>, a forwarded {#snippet header()} and a wrapper that passes content into an island all work, because ogygia sends every snippet given to an island across as a region. This is the snippet form of the same primitive behind <Component/> regions, and you almost never call anything for it: children or a {#snippet} passed into an island work as they would anywhere else, including when a plain wrapper forwards them. The only time you reach for an API is when you build a snippet by hand from raw HTML, which is what region.snippet() is for.

The two shapes of a crossing

A snippet crosses in one of two ways, and ogygia picks the way based on whether the snippet has to wake:

  • Static. Plain content (prose, a code block, markup with no interactivity) renders once on the server and freezes to HTML. The client adopts that HTML in place, so nothing runs again and nothing ships. This is the common case for children.
  • Live. A named {#snippet} whose body has to wake, because it renders an island or is interactive, compiles to its own entry and wakes on the far side. It renders inline on the server and hydrates on the client, so it is interactive where it lands, even after a boundary has forwarded it. Parameters are supported, and the arguments of each call ride across.
<script>
  import Tabs from './Tabs.svelte' with { wake: 'load' };
</script>

<Tabs>
  <p>Frozen prose — crosses static, ships no JS.</p>

  {#snippet toolbar()}
    <LiveButton />   <!-- crosses live: interactive inside the island -->
  {/snippet}
</Tabs>

Islands nested inside static content keep working. They render as full regions and wake on their own, even though the content around them is frozen.

A live snippet is part of the island that renders it, which has these consequences:

  • An island inside it renders inline and hydrates with that island, the same as one placed in the island’s own template.
  • A server island (render: 'deferred') inside it stays deferred. Its fallback ships with the page, its HTML arrives on its schedule, and its code never ships to the browser.
  • It can render the host’s template-root snippets, and read the host’s <script module> imports and values.
  • A lake (wake: 'none') inside it stays frozen, and an island inside that lake wakes on its own schedule.
  • It reads the page’s contexts, on the server and in the browser.
  • The island sets data-hydrated and fires ogygia:hydrated only after its live snippets have hydrated too.

Forwarding through a wrapper

Composition is where this matters. A plain (non-island) component can forward its children into an island, and the crossing happens at the island’s boundary, so the wrapper is ordinary Svelte with no special API:

<!-- a plain wrapper -->
<script>
  import Inner from './Inner.svelte' with { wake: 'load' };
  let { children } = $props();
</script>

<Inner>{@render children()}</Inner>

Building one by hand: region.snippet()

When you need a snippet made from raw HTML, use region.snippet(), the counterpart of region() for components. It mirrors Svelte’s createRawSnippet, except that the result is a region snippet, which you can render in the same graph with {@render} and which can cross a boundary.

You can lift an existing snippet that takes no parameters, which freezes it as static content:

{#snippet badge()}<span class="badge">stable</span>{/snippet}

<script>
  import { region } from 'ogygia';
  const frozen = region.snippet(badge);
</script>

Or you can build one from raw HTML, with the same { render, setup?, captures? } contract as createRawSnippet:

import { region } from 'ogygia';

const stamp = region.snippet({
  render: (label) => `<span class="stamp">${label}</span>`,
  setup: (el) => {
    // client-only wiring; dropped when the snippet is reduced across a wire
  },
  captures: ['shipped']   // serializable inputs, same law as island props
});

render must return one root element, which is the createRawSnippet rule, and captures must be serializable, the same as island props. setup runs on the client after mount. When the snippet crosses a boundary, setup is dropped, because a frozen snippet has nothing to start.