🍡 mochi

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:

  • isHydratabletrue when the call site uses mochi:hydrate, mochi:hydrate:visible, or mochi: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} />