SSR framework for Svelte 5 + Bun with islands-based selective hydration
On this page
Selective hydration with mochi:hydrate
Components render server-side by default and ship zero JavaScript. Add mochi:hydrate to opt a component into client-side hydration; everything else stays static HTML.
<!-- file: src/routes/Page.svelte -->
<Counter mochi:hydrate count={5} />
<StaticHeader />Props are serialized with devalue into a <script type="application/json"> block emitted just before the island, so the same values are available during hydration. See Passing props to islands for the supported types.
Do NOT nest mochi:hydrate (or mochi:hydrate:visible) inside another hydratable component; instead, remove the inner directive and let the outer island hydrate the whole subtree. Hydration is all-or-nothing per island — the framework rejects nested directives at compile time.
The isHydratable prop
Every island invocation receives one implicit prop from the framework:
isHydratable—truewhen the call site usesmochi:hydrate,mochi:hydrate:visible, ormochi:defer mochi:hydrate. Undefined for pure SSR-only invocations.
Accept it in the component’s $props() to branch on hydration state at the same call site that opts in:
<!-- file: src/lib/Counter.svelte -->
<script lang="ts">
let {
isHydratable,
count = 0,
}: {
isHydratable?: boolean;
count?: number;
} = $props();
</script>
{#if isHydratable}
<button onclick={() => count++}>{count}</button>
{:else}
<span>{count}</span>
{/if}Do NOT declare isHydratable as a user-controlled prop; instead, treat it as a read-only input from the framework.
Unique ids with $props.id()
For a unique, SSR-stable id inside an island, use Svelte’s native $props.id() — the value generated during the server render is reused on hydration:
<!-- file: src/lib/SignupField.svelte -->
<script lang="ts">
const uid = $props.id();
</script>
<label for="{uid}-email">Email</label>
<input id="{uid}-email" type="email" />Each component instance gets its own id, so repeating the same island on a page never produces duplicate DOM ids. It also works inside server islands: their standalone renders are namespaced with the island id carried inside the signed props envelope (via render’s idPrefix), so ids from a deferred fragment cannot collide with ids already on the page.
mochi:hydrate:visible
Use mochi:hydrate:visible to defer hydration until the component scrolls into view. The component still server-renders; only its JS (and CSS) load on first intersection.
<HeavyChart mochi:hydrate:visible />
<HeavyChart mochi:hydrate:visible={{ rootMargin: '200px' }} />Pass rootMargin to start loading before the component enters the viewport. See Lazy hydration with mochi:hydrate:visible for the full options.
mochi:defer
Use mochi:defer to render the component on a separate request after the page ships, and combine it with mochi:hydrate to also hydrate the deferred markup once it lands. See Server islands with mochi:defer for the full lifecycle.
<!-- Server-rendered after page load, then hydrated -->
<ShoppingCart mochi:defer mochi:hydrate items={initialItems} />