🍡 mochi
← All demos

Island Props

The parent has no mochi:hydrate directive, so its own code never ships to the browser — only the child island does. Watch how the props (a Date, a Map, a Set) cross the SSR → client boundary intact.

ServerRenderedParent.svelte

Runs only on the server. No mochi:hydrate directive, so this component's own code ships zero JavaScript — its output is inert HTML. It builds a props bag with mixed types and hands it off to the child below.

const user = { name: 'Ada', id: 42 };
const visitedAt = new Date();
const tags = new Set(['svelte', 'bun', 'islands']);
const scores = new Map([['speed', 95], ['dx', 88], ['size', 72]]);
ClientRenderedChild.svelte Server (SSR)
PropValueRuntime type
user{ name: "Ada", id: 42 }Object
visitedAt2026-05-05T15:50:44.096ZDate
tagsSet(3) { svelte, bun, islands }Set
scoresMap(3) { speed => 95, dx => 88, size => 72 }Map

How the props get there

  1. At SSR time, Mochi calls devalue.stringify on the props you pass to the island. Unlike JSON.stringify, it preserves Date, Map, Set, BigInt, RegExp, URL, typed arrays, undefined, NaN, and even cyclic references.
  2. The serialized string is placed on the <mochi-hydratable-island> custom element as a props attribute. (When two islands share the exact same payload, it gets hoisted into a shared <script type="application/json"> block and the attribute becomes a props-ref pointer instead.)
  3. In the browser, the custom element's connectedCallback reads that string and runs devalue.parse on it, reconstructing the rich types.
  4. The result is handed to Svelte's hydrate(...) as the component's props — and the child takes over.

Try it: view source on this page (or inspect the mochi-hydratable-island element) and you'll see the serialized props on the props attribute. Functions, class instances, and Symbols can't be serialized — pass a plain-data representation, or compute them after hydration.

<script lang="ts">
  import ClientRenderedChild from './ClientRenderedChild.svelte';

  const user = { name: 'Ada', id: 42 };
  const visitedAt = new Date();
  const tags = new Set(['svelte', 'bun', 'islands']);
  const scores = new Map<string, number>([
    ['speed', 95],
    ['dx', 88],
    ['size', 72],
  ]);
</script>

<section class="parent">
    <header>
      <h2>ServerRenderedParent.svelte</h2>
      <p>
        Runs only on the server. No <code>mochi:hydrate</code> directive, so this component's own code ships zero JavaScript — its output is inert HTML. It builds a props bag with mixed
        types and hands it off to the child below.
      </p>
    </header>
    <pre><code
        >{`const user = { name: 'Ada', id: 42 };
const visitedAt = new Date();
const tags = new Set(['svelte', 'bun', 'islands']);
const scores = new Map([['speed', 95], ['dx', 88], ['size', 72]]);`}</code
      ></pre>
  </section>

  <div class="arrow" aria-hidden="true">
    <span>props</span>
    <span class="line"></span>
    <span class="head"></span>
  </div>

  <ClientRenderedChild {user} {visitedAt} {tags} {scores} mochi:hydrate />

  <section class="explainer">
    <h3>How the props get there</h3>
    <ol>
      <li>
        At SSR time, Mochi calls <a href="https://github.com/Rich-Harris/devalue" target="_blank" rel="noreferrer"><code>devalue.stringify</code></a>
        on the props you pass to the island. Unlike <code>JSON.stringify</code>, it preserves
        <code>Date</code>, <code>Map</code>, <code>Set</code>, <code>BigInt</code>,
        <code>RegExp</code>, <code>URL</code>, typed arrays, <code>undefined</code>,
        <code>NaN</code>, and even cyclic references.
      </li>
      <li>
        The serialized string is placed on the <code>&lt;mochi-hydratable-island&gt;</code> custom element as a <code>props</code> attribute. (When two islands share the exact same
        payload, it gets hoisted into a shared <code>&lt;script type="application/json"&gt;</code> block and the attribute becomes a <code>props-ref</code> pointer instead.)
      </li>
      <li>
        In the browser, the custom element's <code>connectedCallback</code> reads that string and runs
        <code>devalue.parse</code> on it, reconstructing the rich types.
      </li>
      <li>
        The result is handed to Svelte's <code>hydrate(...)</code> as the component's props — and the child takes over.
      </li>
    </ol>
    <p class="hint">
      <strong>Try it:</strong> view source on this page (or inspect the
      <code>mochi-hydratable-island</code>
      element) and you'll see the serialized props on the
      <code>props</code> attribute. Functions, class instances, and Symbols can't be serialized — pass a plain-data representation, or compute them after hydration.
    </p>
  </section>
Styles
<style>
  .parent {
    border: 2px dashed var(--border);
    border-radius: var(--radius-md);
    padding: 1rem 1.25rem;
    background: var(--surface);
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
  }

  .parent header {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
  }

  .parent h2 {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 1rem;
    font-weight: 600;
    color: var(--text);
  }

  .parent p {
    margin: 0;
    color: var(--text-muted);
    font-size: 0.95rem;
    line-height: 1.5;
  }

  .parent code {
    font-family: var(--font-mono);
    background: var(--surface-muted);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 0.1em 0.35em;
    border-radius: 4px;
    font-size: 0.85em;
  }

  pre {
    margin: 0;
    padding: 0.75rem 0.85rem;
    background: var(--surface-muted);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    overflow-x: auto;
    font-size: 0.85rem;
    line-height: 1.5;
  }

  pre code {
    background: transparent;
    border: none;
    padding: 0;
    font-family: var(--font-mono);
    color: var(--text);
  }

  .arrow {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    color: var(--text-subtle);
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    margin: 0.4rem 0;
  }

  .arrow .line {
    width: 2px;
    height: 1.25rem;
    background: var(--border-strong);
  }

  .arrow .head {
    color: var(--border-strong);
    line-height: 1;
  }

  .explainer {
    margin-top: 1.5rem;
    padding: 1rem 1.25rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    color: var(--text);
  }

  .explainer h3 {
    margin: 0 0 0.6rem 0;
    font-family: var(--font-serif);
    font-size: 1.1rem;
    font-weight: 500;
  }

  .explainer ol {
    margin: 0;
    padding-left: 1.25rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    font-size: 0.95rem;
    line-height: 1.55;
    color: var(--text-muted);
  }

  .explainer code {
    font-family: var(--font-mono);
    background: var(--surface-muted);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 0.1em 0.35em;
    border-radius: 4px;
    font-size: 0.85em;
  }

  .explainer a {
    color: var(--accent);
  }

  .hint {
    margin: 0.85rem 0 0 0;
    padding: 0.75rem 0.85rem;
    background: var(--surface-muted);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-sm);
    font-size: 0.92rem;
    line-height: 1.55;
    color: var(--text-muted);
  }

  .hint strong {
    color: var(--text);
  }
</style>