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.
The isHydratable prop
Every island invocation receives one implicit prop from the framework:
isHydratable—truewhen the call site usesmochi:hydrate,mochi:hydrate:visible,mochi:clientOnly,mochi:clientOnly:visible, ormochi:defer mochi:hydrate. Undefined for pure SSR-only invocations. Formochi:clientOnly*islands it is alwaystrue(they never server-render, so the component only ever runs at client mount).
mochi:hydrate* and mochi:clientOnly* islands receive no islandId prop — for a unique id, use Svelte’s $props.id(). Server islands (mochi:defer) are the exception: they carry an islandId inside their encrypted props envelope as the render’s idPrefix.
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}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 encrypted 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:clientOnly
Use mochi:clientOnly to skip SSR entirely — the component is mounted in the browser only, with an optional fallback snippet as the SSR placeholder. See Client-only components with mochi:clientOnly.
<!-- Never server-rendered; mounts in the browser -->
<AudioVisualizer mochi:clientOnly />Add :visible to defer the browser mount until the placeholder scrolls into view, with the same rootMargin option:
<!-- Never server-rendered; mounts when scrolled into view -->
<AudioVisualizer mochi:clientOnly:visible={{ rootMargin: '200px' }} />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} />Add :visible to defer the fetch until the placeholder scrolls into view, with the same rootMargin option (and combinable with mochi:hydrate*):
<!-- Fetched only when scrolled into view -->
<UserAvatar mochi:defer:visible={{ rootMargin: '200px' }} userId={123} />See it in action
Live demos showing key concepts from this page