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

  1. The island loader is already included in the default theme via theme.yaml:
assets:
  js:
    - js/island-loader.js
  1. 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>
  1. 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:

  1. Finds all <accent-island> elements on the page
  2. Reads the data-hydrate attribute to determine when to activate each island
  3. When the hydration condition is met, calls the registered init function with the element and parsed data-props
  4. 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

AttributeRequiredDescription
data-componentYesName of the island component (maps to a registered init function)
data-hydrateNoHydration strategy: load, idle (default), visible, media
data-propsNoJSON-encoded props passed to the init function
data-mediaNoCSS media query, required when data-hydrate="media"
data-hydratedSet by loaderSet to "true" after successful hydration

Hydration Strategies

Choose when each island activates based on its importance to the user experience:

StrategyWhen It HydratesBrowser APIUse Case
loadImmediately when the script runsDirect callCritical UI: navigation, dark mode
idleWhen the browser is idlerequestIdleCallbackSecondary UI: search, analytics
visibleWhen scrolled into viewIntersectionObserverBelow-the-fold: galleries, comments
mediaWhen a CSS media query matchesmatchMediaResponsive: 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:

FieldRequiredDefaultDescription
hydrateNo"idle"Hydration strategy: load, idle, visible, media
mediaNoNoneCSS media query for media hydration strategy
codeNoNoneCode text rendered as <pre><code> fallback inside the island
fallbackNoNonePlain text rendered as fallback content inside the island
placeholderNoNoneRenders a search <input> with this placeholder text (for search islands)
langNoNoneLanguage for syntax highlighting the code field (e.g., "rust", "js")
All other fieldsNoN/APassed 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:

  1. Matches fenced code blocks where the language starts with island:
  2. Extracts the component name from the language suffix
  3. Parses the JSON body to extract hydrate and media fields
  4. Emits an <accent-island> custom element with the appropriate attributes
  5. Records the island in page.islands for 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
# 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>
PropDefaultDescription
label"Copy"Button text
copied_label"Copied!"Text shown after copying

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>
PropDefaultDescription
endpoint"/_search/index.json"URL to the search index
max_results10Maximum results shown
min_chars2Minimum query length before searching
debounce300Debounce 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"
FieldRequiredDefaultDescription
jsYesPath to the JavaScript file, relative to the plugin directory
default_hydrateNo"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.