Hydration Modes
The same component rendered four different ways. Watch each card turn green when it hydrates — click its button to confirm the JavaScript has taken over. Scroll to reach the lazy islands.
No directive
Pure SSR — no client JavaScript is shipped for this component. It renders on the server and is inert in the browser. Best for content that never needs interactivity.
This button won't work since the component is not hydrated!
mochi:hydrate
Eager hydration — the island boots as soon as its bundle arrives, making it interactive immediately. Use this for above-the-fold UI that the user will touch right away.
This button won't work since the component is not hydrated!
mochi:hydrate:visible
Lazy hydration — the bundle and CSS are only fetched once the island scrolls into view (via an IntersectionObserver). The card below stays inert until you
scroll it into the viewport.
↓ Scroll down ↓
This button won't work since the component is not hydrated!
mochi:hydrate:visible={{ rootMargin }}
You can pass IntersectionObserver options — here rootMargin: '200px' starts hydration a bit before the island is actually in view.
This button won't work since the component is not hydrated!
mochi:defer
Server island — the component is not rendered with the initial page. Instead, a placeholder ships and the browser fetches the rendered HTML from the server
on-demand. Pair with mochi:hydrate to also make it interactive after it loads.
<script lang="ts">
import HydrationTarget from './HydrationTarget.svelte';
</script>
<section class="mode">
<header>
<h2>No directive</h2>
<p>Pure SSR — no client JavaScript is shipped for this component. It renders on the server and is inert in the browser. Best for content that never needs interactivity.</p>
</header>
<HydrationTarget label="<HydrationTarget />" />
</section>
<section class="mode">
<header>
<h2><code>mochi:hydrate</code></h2>
<p>
Eager hydration — the island boots as soon as its bundle arrives, making it interactive immediately. Use this for above-the-fold UI that the user will touch right away.
</p>
</header>
<HydrationTarget mochi:hydrate label="<HydrationTarget mochi:hydrate />" />
</section>
<section class="mode">
<header>
<h2><code>mochi:hydrate:visible</code></h2>
<p>
Lazy hydration — the bundle and CSS are only fetched once the island scrolls into view (via an <code>IntersectionObserver</code>). The card below stays inert until you
scroll it into the viewport.
</p>
</header>
<div class="spacer">
<p>↓ Scroll down ↓</p>
</div>
<HydrationTarget mochi:hydrate:visible label="<HydrationTarget mochi:hydrate:visible />" />
</section>
<section class="mode">
<header>
<h2><code>mochi:hydrate:visible={'{{ rootMargin }}'}</code></h2>
<p>
You can pass <code>IntersectionObserver</code> options — here
<code>rootMargin: '200px'</code>
starts hydration a bit before the island is actually in view.
</p>
</header>
<HydrationTarget mochi:hydrate:visible={{ rootMargin: '200px' }} label={`<HydrationTarget mochi:hydrate:visible={{ rootMargin: '200px' }} />`} />
</section>
<section class="mode">
<header>
<h2><code>mochi:defer</code></h2>
<p>
Server island — the component is <em>not</em> rendered with the initial page. Instead, a placeholder ships and the browser fetches the rendered HTML from the server
on-demand. Pair with <code>mochi:hydrate</code> to also make it interactive after it loads.
</p>
</header>
<HydrationTarget mochi:defer mochi:hydrate label="<HydrationTarget mochi:defer mochi:hydrate />">
<div class="placeholder">Loading from server…</div>
</HydrationTarget>
</section>
Styles
<style>
.mode {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 2.5rem;
}
.mode header {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.mode h2 {
margin: 0;
font-size: 1.05rem;
}
.mode h2 code,
.mode p 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;
font-weight: 500;
}
.mode p {
margin: 0;
color: var(--text-muted);
font-size: 0.95rem;
line-height: 1.5;
}
.spacer {
height: 60vh;
display: flex;
align-items: center;
justify-content: center;
border: 2px dashed var(--border);
border-radius: var(--radius-md);
color: var(--text-subtle);
font-size: 0.9rem;
}
.spacer p {
margin: 0;
}
.placeholder {
padding: 1rem;
border: 2px dashed var(--border-strong);
border-radius: var(--radius-md);
color: var(--text-subtle);
text-align: center;
font-size: 0.9rem;
}
</style>