Dynamic islands
Static HTML.
Dynamic Components.
Some content shouldn't be in the prerendered HTML: cart state, recently viewed products, logged-in user widgets, etc. Islands let you punch those holes through the static page and fill them in the browser, after hydration. It's like a window to your app, within a static page.
The problem islands solve
Prerendered HTML is the same for every visitor. That's what makes it fast and crawlable. But some content is inherently per-visitor: what's in their cart, which pages they've seen, what they've favorited. You can't bake that into static HTML at build time.
How it works
Place a <pre-island> in your JSX
The custom element ships in the prerendered HTML as an inert placeholder. Crawlers see the fallback content inside it. React's renderToString passes unknown elements through unchanged.
<pre-island data-pre-island="cart-widget" data-pre-load="visible">
<span class="island-loading">Loading cart...</span>
</pre-island>Register the component in AppIslands.jsx
One file maps island names to React components. That's the entire registry. No config, no decorators, no build plugin.
import CartWidget from './islands/CartWidget.jsx'
export const islands = {
'cart-widget': CartWidget,
}mountIslands() does the rest
Called in main.jsx after hydrateRoot. Scans the DOM for pre-island elements, finds the matching component, and calls ReactDOM.createRoot(el).render() into each one. Each island is its own React root, independent of the main tree.
import { mountIslands } from './islands.js'
import { islands } from './AppIslands.jsx'
// after hydrateRoot / createRoot:
mountIslands(islands)Load strategies
The data-pre-load attribute controls when each island mounts. Match the strategy to the priority of the content.
Immediate
Default. Mounts right after mountIslands() runs. Use for above-the-fold widgets that need to be interactive quickly.
On scroll
Mounts when the element enters the viewport via IntersectionObserver. Use for below-the-fold content that isn't needed until the user reaches it.
Background
Mounts via requestIdleCallback during browser downtime. Use for low-priority widgets that shouldn't compete with paint or interaction.
What islands can't do
Each island is a separate ReactDOM.createRoot, outside the main hydrateRoot tree. React context from the parent app doesn't reach them. Pass data via data attributes on the element itself, read from localStorage, or fetch from an API.
The component never runs during SSR. Crawlers see the fallback text inside the pre-island element. If you need dynamic content indexed, prerender it into a dedicated route instead.
They're browser-only. If your dynamic content requires auth, per-request data, or database access, you need edge SSR or an API endpoint. Islands handle client state, localStorage, and public fetches well. Everything else belongs server-side.
Live demo
The widget below is a pre-island with data-pre-load="idle". It tracks which pages you've visited this session using sessionStorage. The prerendered HTML for this page contains none of this -- a fallback line is what the crawler sees. Navigate to a few pages and come back.
pre-island and you'll find the placeholder with the fallback text and nothing else. The session data below was never on the server.Session data loads after hydration.