A new performance-focused way to add interactive client-side components to your web site.
Or, more technically: a framework independent partial hydration islands architecture implementation.
- View the demos
- Check out the initial screencast on Eleventy’s YouTube channel and the follow up screencast with more framework examples.
- Learn more about Islands Architecture
Features:
- Easy to add to existing components
- Zero dependencies
- Not tightly coupled to a server framework or site generator tool.
- Small footprint (5 kB minimized; 1.61 kB with Brotli compression)
- Server-rendered (SSR) component examples available for SSR-friendly frameworks (Lit, Svelte, Vue, Preact are provided)
Examples for:
- Web Components
- Web Components using Declarative Shadow DOM (New in v3.0.0)
- Svelte (and SSR)
- Vue (and petite-vue) (and SSR)
- Preact (and SSR)
- Lit (and SSR)
- Alpine.js
- Embedded in Markdown
defer-hydration
Component Attribute Support (New in v3.0.0) (Read more aboutdefer-hydration
at zachleat.com)- Experimental: Image Loading
- Experimental: Using import maps to simplify import URLs. This is for-future-reference when browser support broadens.
Integrations in the wild:
npm install @11ty/is-land
Add is-land.js
to your primary bundle. It can be deferred and/or loaded asynchronously.
When using with web components it must be the first custom element defined (via customElements.define
) on the page. Choose your style:
<script type="module" src="/is-land.js"></script>
<script type="module">
import "/is-land.js";
</script>
The framework autoinit
examples shown below now require a separate is-land-autoinit.js
file too.
<is-land>This is an island.</is-land>
Add any number of loading conditions to this tag to control how and when the island is initialized. You can mix and match:
on:visible
on:idle
on:interaction
(defaults totouchstart,click
)- Change events with
on:interaction="mouseenter,focusin"
- Change events with
on:media
- Viewport size:
on:media="(min-width: 64em)"
- Reduced motion:
- Opt-in with
on:media="(prefers-reduced-motion)"
- Opt-out with
on:media="(prefers-reduced-motion: no-preference)"
- Opt-in with
- Viewport size:
- Save Data (read about Save Data on MDN)
- Opt-in with
on:save-data
- Opt-out with
on:save-data="false"
- Opt-in with
<is-land on:visible on:idle>
<!-- your HTML here -->
<is-land on:media="(min-width: 64em)">
<!-- Islands can be nested -->
<!-- Islands inherit all of their parents’ loading conditions -->
</is-land>
</is-land>
<is-land on:visible on:idle>
<vanilla-web-component>
Put your pre-JS fallback content in your web component.
</vanilla-web-component>
</is-land>
Place any post-JS content inside of one or more <template data-island>
elements anywhere in the <is-land>
. These will be swapped with their template content. You can nest an <is-land>
in there if you want!
<is-land on:visible on:idle>
<template data-island>
<vanilla-web-component>
This component is post-JS.
</vanilla-web-component>
</template>
</is-land>
- Use
data-island="replace"
to replace the contents of the<is-land>
with the template. - Use
data-island="once"
to run a template’s contents once per page (keyed from template contents). (New in v2.0.1)
Embed a script inside the template to run custom JS when the island’s loading conditions have been satisfied!
<is-land on:visible>
<template data-island>
<!-- CSS -->
<style>/* My custom CSS */</style>
<link rel="stylesheet" href="my-css-file.css">
<!-- JS -->
<script type="module">console.log("Hydrating!");</script>
<script type="module" src="my-js-file.js"></script>
</template>
</is-land>
You can also use the ready
attribute for styling, added to the <is-land>
when the island has been hydrated.
<style>
is-land[ready] {
background-color: lightgreen;
}
</style>
Demos and source in the HTML are available for each framework listed here. These examples require the is-land-autoinit.js
JavaScript file (in addition to is-land.js
).
Use the autoinit
and import
attributes together to import a third party library (or component code). autoinit
can be one of petite-vue
, vue
, preact
, or svelte
. It is recommended to use a self-hosted framework library (future Eleventy integrations will automate this for you).
<is-land on:visible autoinit="petite-vue" import="https://unpkg.com/[email protected]/dist/petite-vue.es.js" v-scope="{ name: 'post-JS' }">
Hello from <span v-html="name">pre-JS</span>
</is-land>
<!-- when import maps support is better, this simplifies with an entry for petite-vue in your import map -->
<is-land on:visible import="petite-vue" v-scope="{ name: 'post-JS' }">
Hello from <span v-html="name">pre-JS</span>
</is-land>
- Small library (8K)
- Rendering modes: Client
- Progressive-enhancement friendly (full control of fallback content)
- Support for
autoinit
<is-land on:visible autoinit="petite-vue" import="https://unpkg.com/[email protected]/dist/petite-vue.es.js" v-scope="{ name: 'post-JS' }">
Hello from <span v-html="name">pre-JS</span>
</is-land>
- Larger library (51 kB)
- Rendering modes: Client-only, Server-only, Server + Client (Hydration)
- Support for
autoinit
<is-land on:visible>
<div id="vue-app">
Hello from <span v-html="name">pre-JS</span>
</div>
<template data-island>
<script type="module">
import { createApp } from "https://unpkg.com/[email protected]/dist/vue.esm-browser.prod.js";
createApp({
data: () => ({ name: "post-JS" })
}).mount("#vue-app")
</script>
</template>
</is-land>
- Smaller library (12 kB)
- Rendering modes: Client-only, Server-only, Server + Client (Hydration)
- Requires a compiler for both client and server modes (tighter server coupling)
- Support for
autoinit
This example uses an Eleventy/Svelte integration to compile a Svelte component.
{% assign component = "./lib/svelte/my-component.svelte" | svelte %}
<is-land on:visible autoinit="svelte" import="{{ component.clientJsUrl }}"></is-land>
Example component code ./lib/svelte/my-component.svelte:
<script>
// using export to allow overrides via props
export let name = 'world';
let count = 0;
function handleClick() {
count += 1;
}
</script>
<style>
h1 { color: red }
</style>
<h1>Hello {name}</h1>
<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
- Very small library (~5 kB)
- Rendering modes: Client-only, Server-only, Server + Client (Hydration)
- Support for
autoinit
This example uses htm
instead of JSX.
<is-land on:visible autoinit="preact" import="/lib/preact/preact-component.js"></is-land>
Example component code ./lib/preact/preact-component.js
:
import { html, render } from 'https://unpkg.com/htm/preact/index.mjs?module'
function App (props) {
return html`<p><strong>Hello ${props.name}!</strong></p>`;
}
export default function(el) {
render(html`<${App} name="from Preact" />`, el);
}
- Small library (~7 kB)
- Rendering modes: Client-only, Server + Client (Hydration)
- Note: Server-only is not supported: it requires Declarative Shadow DOM support to work without JS.
- No support for
autoinit
<is-land on:visible import="./lib/lit/lit-component.js">
<lit-component name="Post-JS">Pre-JS Content</lit-web-component>
</is-land>
Example component code ./lib/lit/lit-component.js
:
import {html, css, LitElement} from "https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js";
customElements.define('lit-component', class extends LitElement {
static properties = {
name: {type: String},
};
render() {
return html`<p>Hello, ${this.name || "Stranger"}!</p>`;
}
});
- Smaller library (15 kB)
- Rendering modes: Client-only
- No
autoinit
but it is not needed (functionality included for-free by Alpine.js)
<is-land on:visible import="https://unpkg.com/alpinejs">
<div x-data="{ count: 0 }">
Hello from Alpine.js!
<button @click="count++">⬆️</button> <button @click="count--">⬇️</button> <span x-text="count"></span>
</div>
</is-land>