Hydration
Svelte’s compiler-generated custom elements cannot hydrate on their own. When an element upgrades, its constructor attaches a shadow root, which per spec clears any declarative shadow root the server delivered, and then mounts the component from scratch. Server-rendered DOM was paint-and-throw-away: good for first paint and SEO, but every node was re-created the moment JavaScript arrived.
Components built with @svebcomponents/build hydrate instead. The
server-rendered shadow DOM is adopted in place: existing nodes are kept,
component styles are reused rather than re-injected, and the component is fully
reactive afterwards.
Hydration is on by default for every SSR-enabled component. There is nothing to configure, but it helps to know how it works and where its limits are today.
How it works
Section titled “How it works”Everything is built on documented Svelte extension points, not forked compiler output:
- During the build, auto-options injects
Svelte’s official
customElement.extendoption, wiring in a wrapper class from@svebcomponents/ssr/hydration. - The wrapper intercepts the
attachShadowcall in the generated element’s constructor and claims the declarative shadow root before it would be cleared. - On connect, the wrapper hydrates the claimed shadow root with Svelte’s
public
hydrate()API instead of mounting. - Both sides render the component through the same internal
HydrationHostcomponent, so the server markup structurally matches what the client expects: the generated SSR entry uses a server-compiled copy, and the client hydrates against it. - The same symmetry protects the element inside hydrating Svelte host apps
such as SvelteKit. The SSR wrapper and the browser wrapper render
structurally identical
<svelte:element>fragments, selected per environment through export conditions rather than a runtime branch, so the host’s hydration claims the server-rendered element instead of re-creating it. Re-creating it would discard the shadow root before the element could hydrate. - Rich, non-attribute props the server rendered with are serialized into the shadow output and restored before hydrating, so host frameworks that only re-supply props after their own asynchronous hydration do not cause a mismatch.
Graceful degradation
Section titled “Graceful degradation”Anything the wrapper cannot hydrate falls back to Svelte’s untouched mount path, which is exactly how all custom elements behaved before hydration existed:
| Situation | Behaviour |
|---|---|
| No declarative shadow root (the element was never server-rendered) | Plain client mount |
| The component declares slots (see below) | Plain client mount |
| Re-connection after teardown (removed long enough for Svelte to destroy it) | Fresh mount |
| Hydration mismatch | Svelte logs a warning, clears the shadow root, re-mounts |
The worst outcome is a re-render, never a broken component.
Limitations
Section titled “Limitations”Components that declare <slot> elements currently fall back to the mount
path. They render correctly, but re-render on upgrade instead of adopting the
server DOM. In dev mode the element logs a console.info when this happens, so
the fallback is never mistaken for broken hydration.
Svelte 5 still compiles <slot> elements in custom-element mode through a
legacy transformation that creates slot elements imperatively, with no
hydration awareness. That transformation is expected to disappear with Svelte
6, at which point literal <slot> elements flow through Svelte’s normal,
hydration-aware element path and this limitation should lift.
Legacy component events
Section titled “Legacy component events”createEventDispatcher events are not forwarded through the hydration host.
Events dispatched via $host(), the
idiomatic pattern for custom elements, bubble natively and work unchanged.
Component exports
Section titled “Component exports”exported component functions are not exposed as element methods on hydrated
hosts.
Bundle cost
Section titled “Bundle cost”Hydration support adds roughly 4–5 KB gzipped to a self-contained
client bundle: Svelte’s hydration internals plus the wrapper. Builds that share
the host application’s Svelte runtime through the
svelte condition
pay less. If a component is only ever used client-side, hydratable: false
reclaims it.
Opting out
Section titled “Opting out”Per package, in svebcomponents.config.ts:
import { defineConfig } from "@svebcomponents/build";
export default defineConfig({ hydratable: false,});Per component, declare your own extend in
<svelte:options customElement={{ ... }}>. auto-options never injects over one
you wrote, so the component keeps Svelte’s default mount behaviour unless your
extension implements hydration itself.