Islands Architecture
Add isolated pockets of client-side interactivity to server-rendered pages while preserving Accent's zero-JS-by-default philosophy.
Try It Live
Click the button to activate real <accent-island> components on this page.
Islands architecture lets you add interactive JavaScript widgets to specific parts of a page while the rest remains static, server-rendered HTML. Pages without islands continue to ship zero JavaScript.
Each island is an <accent-island> custom element that the island loader discovers and hydrates according to a scheduling strategy. Server-rendered fallback content inside the element works without JavaScript, so the page is always functional.
Quick Start
- The island loader is already included in the default theme via
theme.yaml:
assets: js: - js/island-loader.js
- Use an island in any template:
<accent-island data-component="copy-code" data-hydrate="idle">
{# Code blocks inside will get a "Copy" button after hydration #}
<pre><code>echo "Hello, world!"</code></pre>
</accent-island>
- The island component script registers itself:
window.AccentIslands.register("copy-code", function (el, props) { // Enhance the element with interactivity });
How It Works
The island loader (island-loader.js) runs on page load and:
- Finds all
<accent-island>elements on the page - Reads the
data-hydrateattribute to determine when to activate each island - When the hydration condition is met, calls the registered init function with the element and parsed
data-props - Sets
data-hydrated="true"on the element after successful hydration
If a component script loads after the loader (e.g., via a deferred script tag), the loader retries pending islands when the component registers itself.
The <accent-island> Element
| Attribute | Required | Description |
|---|---|---|
data-component | Yes | Name of the island component (maps to a registered init function) |
data-hydrate | No | Hydration strategy: load, idle (default), visible, media |
data-props | No | JSON-encoded props passed to the init function |
data-media | No | CSS media query, required when data-hydrate="media" |
data-hydrated | Set by loader | Set to "true" after successful hydration |
Hydration Strategies
Choose when each island activates based on its importance to the user experience:
| Strategy | When It Hydrates | Browser API | Use Case |
|---|---|---|---|
load | Immediately when the script runs | Direct call | Critical UI: navigation, dark mode |
idle | When the browser is idle | requestIdleCallback | Secondary UI: search, analytics |
visible | When scrolled into view | IntersectionObserver | Below-the-fold: galleries, comments |
media | When a CSS media query matches | matchMedia | Responsive: mobile-only sidebar |
If requestIdleCallback is unavailable, idle falls back to setTimeout(200). If IntersectionObserver is unavailable, visible falls back to load.
Example: Deferred Search Widget
<accent-island data-component="search" data-hydrate="visible" data-props='{"endpoint": "/_search/index.json", "max_results": 5}'> <!-- Fallback: plain HTML form that works without JS --> <form action="/search" method="get"> <input type="search" name="q" placeholder="Search..."> <button type="submit">Search</button> </form> </accent-island>
The search form works without JavaScript. When the island scrolls into view, the search component enhances it with client-side filtering.
Example: Mobile-Only Island
<accent-island data-component="mobile-menu" data-hydrate="media" data-media="(max-width: 768px)"> <nav><!-- menu links --></nav> </accent-island>
This island only hydrates on screens narrower than 768px. On desktop, the static HTML is used as-is.
Writing Island Components
An island component is a JavaScript file that registers an init function with the loader. Place component scripts in your theme’s assets/js/islands/ directory.
Minimal Component
window.AccentIslands.register("my-widget", function (el, props) { // `el` is the <accent-island> DOM element // `props` is the parsed JSON from data-props (or {} if absent) el.querySelector("button").addEventListener("click", function () { // Add interactivity here }); });
Component Guidelines
- Progressive enhancement: The server-rendered HTML inside
<accent-island>must be functional without JavaScript. The init function enhances it, not replaces it. - Isolation: Each island is independent. Do not rely on other islands being hydrated.
- Error handling: If your init function throws, the loader catches the error and logs a warning. Other islands on the page are unaffected.
- Framework-agnostic: Use vanilla JavaScript, or bring any framework (Preact, Alpine.js, Lit) as long as the component registers via
AccentIslands.register.
Loading Component Scripts
Add island component scripts to theme.yaml:
assets: js: - js/island-loader.js - js/islands/copy-code.js - js/islands/search.js
Or load them manually in a template for per-page control:
<script src="/theme/assets/js/islands/copy-code.js" defer></script>
Markdown Island Directives
Content authors can embed islands directly in markdown using fenced code blocks with island:<component> as the language identifier. No template editing required.
Syntax
```island:chart { "type": "bar", "data": [10, 20, 30], "hydrate": "visible" } ```
The JSON body supports these fields:
| Field | Required | Default | Description |
|---|---|---|---|
hydrate | No | "idle" | Hydration strategy: load, idle, visible, media |
media | No | None | CSS media query for media hydration strategy |
code | No | None | Code text rendered as <pre><code> fallback inside the island |
fallback | No | None | Plain text rendered as fallback content inside the island |
placeholder | No | None | Renders a search <input> with this placeholder text (for search islands) |
lang | No | None | Language for syntax highlighting the code field (e.g., "rust", "js") |
| All other fields | No | N/A | Passed as data-props to the island component |
The hydrate, media, code, and fallback fields are extracted from the JSON. Remaining fields become the data-props JSON string.
The code field is especially useful for copy-code islands, providing the code block that the component enhances with a Copy button. Add lang for syntax highlighting:
```island:copy-code { "hydrate": "idle", "code": "fn main() {\n println!(\"Hello\");\n}", "lang": "rust" } ```
Use \n for newlines in the JSON string. Without lang, the code renders as plain text. With lang, it gets server-side syntax highlighting via the configured theme.
How It Works
The IslandProcessor runs in the markdown rendering pipeline and:
- Matches fenced code blocks where the language starts with
island: - Extracts the component name from the language suffix
- Parses the JSON body to extract
hydrateandmediafields - Emits an
<accent-island>custom element with the appropriate attributes - Records the island in
page.islandsfor automatic script loading
Automatic Script Loading
When a page contains island: directives, the base template automatically loads the required component scripts. No manual <script> tags needed.
For example, a page with island:chart and island:search blocks will automatically load chart.js and search.js from the theme’s assets/js/islands/ directory. Pages without any island directives load zero island JavaScript.
Empty Body
An island directive with no configuration is valid – it uses the default hydration strategy (idle) and no props:
```island:copy-code ```
Error Handling
If the JSON body is invalid, the directive is rendered as a regular code block (the page does not break). This means island directives degrade gracefully in any context:
- In Accent CMS: transformed into interactive
<accent-island>elements - In GitHub/VS Code preview: rendered as a JSON code block (harmless)
- With invalid JSON: rendered as a regular code block with a warning
Example: Chart and Search
# Monthly Report Revenue grew 15% month-over-month. ```island:chart { "type": "bar", "data": {"labels": ["Oct", "Nov", "Dec"], "values": [42000, 45000, 48000]}, "hydrate": "visible" } ``` Use the search below to find related reports: ```island:search { "hydrate": "visible", "endpoint": "/_search/index.json", "max_results": 5 } ```
Built-in Island Components
The default theme ships with two example islands:
copy-code
Adds a “Copy” button to code blocks inside the island.
<accent-island data-component="copy-code" data-hydrate="idle"> <pre><code>cargo run -- serve</code></pre> </accent-island>
| Prop | Default | Description |
|---|---|---|
label | "Copy" | Button text |
copied_label | "Copied!" | Text shown after copying |
search
Enhances a search form with client-side filtering against a JSON index.
<accent-island data-component="search" data-hydrate="visible" data-props='{"endpoint": "/_search/index.json"}'> <form action="/search" method="get"> <input type="search" name="q" placeholder="Search..."> </form> </accent-island>
| Prop | Default | Description |
|---|---|---|
endpoint | "/_search/index.json" | URL to the search index |
max_results | 10 | Maximum results shown |
min_chars | 2 | Minimum query length before searching |
debounce | 300 | Debounce delay in milliseconds |
Frontmatter-Configured Islands
Using custom frontmatter passthrough, pages can declare islands in their frontmatter and render them in templates:
--- title: "Product Catalog" islands: filter: component: "product-filter" hydrate: "load" props: categories: ["electronics", "clothing", "books"] ---
{% if page.custom.islands %}
{% for name, config in page.custom.islands.items() %}
<accent-island
data-component="{{ config.component }}"
data-hydrate="{{ config.hydrate | default('idle') }}"
data-props='{{ config.props | tojson }}'>
</accent-island>
{% endfor %}
{% endif %}
Plugin-Provided Islands
Note: Plugin-provided islands are an in-progress capability and may not be available in your build. Template-driven and markdown islands are stable.
WASM plugins can register island components alongside their server-side logic. Content authors use plugin islands identically to theme islands – the island: directive syntax is the same.
Declaring Islands in plugin.toml
Plugins declare islands in their plugin.toml manifest:
[plugin]
name = "my-plugin"
version = "0.1.0"
api_version = "0.1.0"
[islands."my-plugin:word-count"]
js = "assets/word-count.js"
default_hydrate = "idle"
[islands."my-plugin:chart"]
js = "assets/chart.js"
default_hydrate = "visible"
| Field | Required | Default | Description |
|---|---|---|---|
js | Yes | – | Path to the JavaScript file, relative to the plugin directory |
default_hydrate | No | "idle" | Default hydration strategy when not specified in the directive |
Namespace the name with your plugin. Island components register into one
global registry per page and the loader has no namespace concept: a plugin
island and a theme island called word-count collide, whichever registers last
wins, and the page renders the wrong component with no diagnostic. Prefixing the
declared name with the plugin’s own name removes the whole class of collision –
accent package and the registry’s checks both warn when it is missing. The
name is a registry key and never a file path, so it need not match the asset’s
filename; a colon in a TOML key means the key has to be quoted, and the
JavaScript must register the same string:
window.AccentIslands.register("my-plugin:word-count", function (el, props) { // ... });
Plugin Asset Serving
Plugin JavaScript assets are served at /assets/plugins/<plugin-name>/<path>. For example:
/assets/plugins/my-plugin/assets/word-count.js
The path is secured with the same traversal hardening as theme assets. Invalid paths return 404.
Using Plugin Islands in Markdown
Content authors use plugin islands with the same island: syntax:
```island:my-plugin:word-count { "target": "article" } ```
The directive names the island exactly as the manifest declared it, namespace
included. One namespace prefix is allowed; both halves accept letters, digits,
- and _, and nothing else – so a name can never become a path.
The island processor checks the plugin registry first, then falls back to theme islands. Plugin islands take priority if both a plugin and the theme define a component with the same name.
The base template automatically generates the correct <script> URL based on whether the island comes from a theme or a plugin:
- Theme islands:
/theme/assets/js/islands/<component>.js - Plugin islands:
/assets/plugins/<plugin-name>/<js-path>
Build Mode
During accent build, plugin JS assets are copied to output/assets/plugins/<plugin-name>/ alongside theme assets. Plugin islands work identically in the static output.
Name Collisions
If two plugins register an island with the same name, the first-loaded plugin wins and a warning is logged. Plugin islands take priority over theme islands of the same name.
Build Mode Compatibility
Islands work in static builds (accent build) because the island loader and component scripts are theme assets (or plugin assets), which are copied to the output directory automatically. The <accent-island> elements are part of the rendered HTML. No running server is needed – the JavaScript loads from the static output.
Edition Availability
Islands – template-driven, markdown directive, and plugin-provided – are available with no license key in accent build and dev serve. Serving in production requires a Standard or Pro license.
In This Chapter
This chapter covers the islands architecture in depth:
- Hydration Patterns – choosing the right hydration strategy (eager, idle, visible, interaction) for each island based on its role, urgency, and position on the page.
- Contact Form Island – a complete worked example: enhancing the contact form with inline validation and fetch-based submission while keeping a working no-JavaScript fallback.
In this section
-
Island Hydration Patterns
Choose the right hydration strategy for each island based on its role, urgency, and position on the page.
-
Contact Form Island
Enhance the contact form with inline validation and fetch-based submission using the islands architecture.