Islands Architecture
Islands architecture is a pattern where you ship mostly static HTML from the server and activate only the interactive regions (islands) with JavaScript on the client. The rest of the page remains static HTML and ships little to no client-side JavaScript unless a region needs it.
Key distinction: Islands architecture is a rendering pattern, not a deployment or team architecture. It controls how a page renders and hydrates. It does not control how you organize teams or codebases.
The problem it solves
Section titled “The problem it solves”Traditional SPAs hydrate the entire page tree, even regions that will never change. This means:
- Larger JavaScript bundles
- Slower time-to-interactive (TTI) on slower networks/devices
- Wasted CPU hydrating static content
- Poorer fallback behavior if JavaScript fails to load
Islands architecture inverts this: ship HTML-first, then opt specific regions into JavaScript when they need it. You send less JavaScript to the client and limit hydration to interactive regions, which improves load and interaction performance.
Quick mental model:
Islands control when and where JavaScript runs. They optimize rendering and hydration within a single application. They do not control team boundaries or independent deployment.
On hydration: mountly hydrates the interactive islands. The rest of the page stays static HTML. The browser runs JavaScript setup (event listeners, state initialization, and so on) for regions that need interactivity and leaves static regions untouched.
How mountly fits
Section titled “How mountly fits”mountly is a component mounting library rather than a full SSR framework.
Your server or framework renders the initial HTML and marks the interactive
regions with data-* attributes; mountly reads them and decides when to load
and mount the client module.
<!-- Server renders the island, and says it is already live --><div data-mountly="/widgets/cart.js" data-mountly-state="mounted" data-props='{"items":3}'> <div class="cart-summary">3 items, $42</div></div>
<script type="module" src="/mountly/auto.js"></script>auto.js (2.3 KB gzipped) wires every island on the page. Islands added later —
by another widget, an htmx swap, a Turbo frame — are picked up automatically, so
nested islands need no ordering configuration.
Two ways in:
- The script tag —
mountly/auto, nothing for you to write - As a dependency —
import { mountly } from "mountly"when you need an alias map, a custom loader, or a scoped root
Either way mountly stays composable with Astro, Next.js, Remix, Rails, Django, CMS templates, or plain server-rendered HTML. Those tools own routing and rendering; mountly owns the portable activation layer.
Trigger types
Section titled “Trigger types”Control when JavaScript loads and runs.
| Trigger | When JS loads | Best for |
|---|---|---|
click |
On first click | Forms, modals, drawers |
hover |
On hover | Menus, previews, tooltip-like widgets |
focus |
On focus | Inputs, search boxes, accessible controls |
idle |
When browser is idle via requestIdleCallback |
Below-the-fold content, low-priority features |
viewport |
When element enters viewport | Lazy sections, cards, comments, feeds |
media |
When a media query matches | Device/layout-specific UI |
url-change |
When URL/history changes | Search/filter panels, route-aware widgets |
never |
Only if manually triggered | Server-rendered features that may never need JS |
Islands hydrate on page load, on user interaction (click, focus, hover), when they enter the viewport, when the browser is idle, or when a media query matches. You choose when JavaScript runs based on user behavior and network conditions.
Island attributes
Section titled “Island attributes”The attributes are the contract between the server-rendered HTML and mountly’s client runtime.
| Attribute | Default | What it controls |
|---|---|---|
data-mountly |
Required | Module URL, or a key in the host script’s data-mountly-urls map |
data-on |
click |
Activation trigger(s). never leaves the island inert until code mounts it |
data-preload |
None | Fetch the module without mounting — usually hover or viewport |
data-props |
{} |
JSON props. A <script type="application/json"> child works too |
data-target |
The island element | Where to mount, when the trigger is not the container |
data-toggle |
Off | A second activation unmounts instead of doing nothing |
data-css |
Sibling .css |
none, or an explicit stylesheet URL |
data-module-url |
data-mountly |
Bundle URL reported to the widget as moduleUrl, when the island loads by key |
data-mountly-state |
Set by mountly | idle / loading / mounted / error — style each with CSS |
A trigger reads as kind or kind:arg, so a trigger never needs a second
attribute to hold its argument:
<div data-mountly="/w.js" data-on="media:(min-width: 60rem)"></div><div data-mountly="/w.js" data-on="viewport:200px"></div><div data-mountly="/w.js" data-preload="hover:300"></div><!-- whichever comes first: mouse users click, keyboard users focus --><button data-mountly="/w.js" data-on="click focus"></button>Anything the attributes can’t express goes through mountly():
import { mountly, mount, unmount, update } from "mountly";
const stop = mountly({ root: document.querySelector("#app"), urls: { cart: "/widgets/cart.js" }, observe: true, // one escape hatch instead of retry / retryDelayMs / auth / test-double knobs load: (url) => import(url),});SSR handoff
Section titled “SSR handoff”There is one rule: data-mountly-state="mounted" in the server’s HTML means
“this island is already live, leave it alone.”
<div data-mountly="/widgets/product-card.js" data-mountly-state="mounted"> <a class="product-card" href="/products/42">View product</a></div>Use when: the server-rendered HTML is complete and functional — product cards, links, summary panels, read-only content.
Omit the attribute and the island activates on its trigger, replacing whatever the server rendered. That is the placeholder case: the client needs state, event listeners, browser APIs, or a different DOM shape from the server fallback.
<div data-mountly="/widgets/counter.js"> <button>Open counter</button></div>If you need to mount over server content anyway, call mount(el) — an explicit
act, rather than a flag the markup carries.
No-JS fallback
Section titled “No-JS fallback”mountly islands can degrade without JavaScript. The server-rendered HTML remains visible even if the JS bundle fails to load or execute.
This requires one pattern:
<head> <!-- Global stylesheet for no-JS fallback --> <link rel="stylesheet" href="/styles.css" /></head>
<body> <div data-mountly="/widgets/card.js"> <span class="styled-widget">Server-rendered content</span> </div>
<!-- Host script that mounts islands when JS is available --> <script type="module" src="/mountly/auto.js"></script></body>When JS is on, mountly can load the component and pass CSS hints through to adapters. By default the widget mounts in light DOM and the document <link> continues to apply; if shadow: true is set, the same CSS is adopted into the shadow root. When JS is off, the global stylesheet keeps the SSR content readable.
This resembles a <noscript> fallback, except the fallback is the server-rendered island content itself.
Honest tradeoffs
Section titled “Honest tradeoffs”mountly islands advantages
Section titled “mountly islands advantages”- No activation API: attributes on the server-rendered region, and a 2.3 KB script tag
- Framework-agnostic widgets: build islands in React, Vue, Svelte, Solid-compatible modules, or plain widget modules
- Host-framework agnostic: works with Astro, Next.js, Remix, server templates, CMS pages, and plain HTML
- Gradual adoption: mix islands and full-page apps on the same site
- No-JS fallback path: keep meaningful HTML in the page before mountly activates anything
mountly islands real constraints
Section titled “mountly islands real constraints”- Light DOM is the default, shadow DOM is opt-in. Light DOM integrates with global styles and form APIs;
shadow: truegives you hard style isolation (good for independent islands embedded in unknown hosts). Pick based on your needs. - Islands stay independent. To share React Context, query clients, or routers across islands, you coordinate them by hand (event bus, URL state, external store).
- Custom elements have form constraints (custom element approach only). When you register custom elements with
defineMountlyFeature(), form integration differs. Use plaindata-mountlyislands if you need native form behavior. - Islands architecture does not address team ownership or independent deployment. If you need team-driven boundaries, separate CI/CD pipelines, or cross-team ownership, microfrontends fit better.
When to use mountly islands
Section titled “When to use mountly islands”- Your backend already renders HTML and you need selective client activation
- You want portable islands without moving the whole site to a new framework
- You are shipping components to unknown hosts, CMS pages, or third-party pages
- You want Astro-like partial hydration behavior in places that are not Astro apps
- You want something that can also compose with Astro rather than replace it
Islands architecture vs. microfrontends
Section titled “Islands architecture vs. microfrontends”Islands architecture and microfrontends solve different problems.
Islands architecture optimizes rendering and hydration within a single application. It controls how regions become interactive and when JavaScript loads, and assumes unified ownership and deployment.
Microfrontends draw independently developed and deployed application boundaries. They solve team autonomy, separate CI/CD pipelines, and cross-team ownership. Islands do not.
If you need team-driven boundaries and independent deployments, consider microfrontends. If you want to optimize JavaScript loading and hydration within one app, islands are the fit.
When to reach for a full islands framework instead
Section titled “When to reach for a full islands framework instead”| Scenario | Consider | Why |
|---|---|---|
| Content-first site (blogs, marketing, docs) | Astro | Built-in SSR, content collections, auto island generation, and integrated routing make this much simpler |
| Deno ecosystem | Fresh | Native islands runtime support; no separate activation layer needed |
| Every region shares app state | Full-page app (React, Next.js, Remix) | If Context, stores, and routers cross island boundaries, one app tree is simpler than coordinating independent islands |
| Custom element form integration | Full-page app | If you need native form submission or deep form API integration in one unified tree, a full-page app model is usually simpler than custom-element boundaries. |
| Team autonomy & independent deployment | Microfrontends | Islands architecture does not solve team boundaries, deployment independence, or cross-team ownership concerns |
Size comparison
Section titled “Size comparison”Representative outcomes for a React island:
| Build | Size impact | Includes |
|---|---|---|
| Self-contained | Largest | React + ReactDOM + adapter + component |
| Peer build | Smaller | Component + adapter, with framework runtime provided by host |
| SSR-only fallback | No client JS for that island | Server-rendered HTML + CSS |
The exact numbers depend on your bundler, framework version, component code, and whether dependencies are externalized. Islands make sense when some regions can avoid JavaScript or load it later. If every region needs immediate shared client state, full-page hydration may be simpler.
How it compares to alternatives
Section titled “How it compares to alternatives”| Feature | mountly Islands | Astro | Fresh | Full-page SPA |
|---|---|---|---|---|
| Server-side rendering | You provide it | Built in | Built in | Usually separate |
| Selective hydration | Yes | Yes | Yes | No |
| Works without JS | Yes, if SSR fallback is meaningful | Yes | Yes | Usually no |
| UI framework choice | Adapter-based | Multi-framework integrations | Preact-first | App choice |
| Host/build-tool agnostic | Yes | No | No | Depends |
| Automatic island generation | No | Yes | Yes | N/A |
| Best fit | Portable activation layer | Content sites | Deno islands apps | Deep client apps |
Next steps
Section titled “Next steps”- Read about triggers in detail: Triggers
- See a complete example: Plain HTML host
- API reference: the core in source