Reference
Constraints & patterns
What crosses the island boundary, and what does not. Each rule is paired with the mistake that it prevents, and the fix.
On this page
Islands are a real boundary. Know what crosses it, and what quietly does not, and you stay out of trouble. Each rule below is the constraint, plus the mistake that people hit first, and the way around it.
Props cross. Children and snippets do not.
Props are serialized with devalue, and cross into the island. Host children, snippets, and callbacks do not. They live in the host scope, not in the scope of the island. You cannot give an island a snippet or a function from the page.
<!-- ✗ the island never sees this snippet or callback -->
<Island onselect={handleSelect}>
{#snippet row(item)}<Row {item} />{/snippet}
</Island>Model the interaction with props and remote functions, or move the UI inside the own .svelte file of the island.
The one reserved exception is ogygiaFallback on a server island. See that page for its handling.
Captured host state is a snapshot
A value that you read from the host into island markup is a serialized snapshot, not live state. A mutation of it updates nothing.
<!-- ✗ writes to a serialized snapshot; nothing updates -->
<button onclick={() => count++}>{count}</button><!-- ✓ own the state inside the island -->
<script>
let count = $state(props.initial);
</script>
<button onclick={() => count++}>{count}</button>ogygia stops the mistake for you in two ways:
| Guard | When | What |
|---|---|---|
| Build error | compile | captured-variable writes in island markup fail the build |
| Dev proxy | runtime | a deep proxy throws on mutation (dev only) |
Pass data as props. Own mutable state inside the island.
Store reads are snapshots too
A $store auto-subscription read inside a snippet that crosses an island boundary is captured the same way. The compiler evaluates the subscription at the host, where the $ sugar is legal. It gives the value across as a plain serialized prop, and rewrites the copy of the snippet to use it. The build warns, with a trace to each read. In native Svelte, $store is live. But across the boundary, it is a frozen render-time snapshot. Later store updates never reach the island.
{#snippet children()}
<!-- captured as the VALUE of $country at render time (build warning points here) -->
<span>{$country}</span>
{/snippet}To make the snapshot explicit (and to silence the warning), read the store at host scope, and use the plain value inside the snippet. The store object itself cannot cross. The functions of a host-declared store do not serialize (the compiler warns here too). For live shared state across the boundary, use a wired class.
Page-level lifecycle is dead code
On a csr = false page, the page module never runs on the client. Top-level onMount, effects, and event listeners in the page do nothing.
<!-- ✗ in a csr = false page, this never fires -->
<script>
onMount(() => startPolling());
</script>When you reach for onMount, you really want an island. Move all that must run in the browser inside one. For init, telemetry, or listeners that render no UI, that is a boot island: a headless component whose full job is its $effect.
Inline <script> tags in page markup are a related trap. They run one time, on the first load, not per navigation. The router swaps <body> and does not execute them. Per-navigation code belongs in an island. See the router for why.
Reach for an island, not a bare dynamic import
When you find yourself hand-rolling import() + mount to lazy-load a component, stop.
// ✗ hand-rolled lazy mount: no SSR HTML, no signed boundary
const { default: Chart } = await import('./Chart.svelte');
mount(Chart, { target, props });That is exactly what wake: 'idle' and wake: 'visible' already do, with SSR HTML first and a signed boundary around it. Use the island.
Choose the boundary before you build
When a subtree needs interactivity, it is an island. When it does not, it is a lake, or plain page HTML. Do not wrap static content in an island to “be safe”. That only ships JS that you did not need.
Decide up front:
| Need | Boundary |
|---|---|
| Interactive | island |
| Static but heavy | lake |
| Request-dependent HTML | server island |
To retrofit the boundary later is the costly path.
Dev is not prod
Two behaviors differ in dev, and both can hide defects until you ship:
- SSR query seeding degrades to a client refetch.
- The mutation-guard proxy runs only in dev.
Something that operates in dev can behave differently in production. Always check data flows against a production build before you ship.