AkurAI Build
Menu

Public repository / fee5b6c2cde89fa6bf8dc51068af7336b56a88b8

AkurAI-Framework / site/frontend/islands.html

Latest change d3227deaa0b875c75ab4eff2f4a9010ce80ce9a1 - feat(frontend): SSR islands hydration runtime (islands.js) + demo + docs by Ólafur Búi Ólafsson

{% include "header" %}
    <main class="container">
      <header class="page-head">
        <span class="eyebrow">Live demo</span>
        <h1>Islands &amp; hydration</h1>
        <p class="lede">The server renders this whole page. Only the regions marked
        <code>data-island</code> become interactive — <code>islands.js</code> hydrates them
        in place, reusing the server-rendered nodes, with no virtual DOM and no flicker. Each
        island starts at the exact state the server printed. <a href="/docs/islands">Read how it
        works →</a></p>
      </header>

      <!-- Static SSR region: NOT an island. Never touched by JS. -->
      <section class="card">
        <header class="rt-head">
          <h3>Static (server-only)</h3>
          <span class="status">no JS</span>
        </header>
        <p class="muted small">This region has no <code>data-island</code>. It is pure
        server-rendered HTML and the hydration runtime leaves it completely alone. View source:
        what the server sent is what you see.</p>
      </section>

      <section class="split rt-grid">
        <!-- Counter island: initial state from a <script type="application/json"> blob -->
        <article class="card" data-island="counter">
          <script type="application/json" data-island-state>{"count": 7, "step": 1}</script>
          <header class="rt-head">
            <h3>Counter island</h3>
            <span class="status ok">data-island="counter"</span>
          </header>
          <p class="muted small">The server printed <code>count: 7</code> in a JSON blob inside
          the island. After hydration the number is live — but it never flashes from 0, because
          the signal is seeded from that printed value.</p>
          <div class="rt-num" id="counter-value">7</div>
          <div class="btn-row">
            <button class="btn btn-secondary" data-counter-dec>−1</button>
            <button class="btn btn-primary" data-counter-inc>+1</button>
            <button class="btn btn-ghost" data-counter-reset>Reset</button>
          </div>
        </article>

        <!-- Toggle island: initial state from a data-state attribute -->
        <article class="card" data-island="toggle" data-state='{"open": true}'>
          <header class="rt-head">
            <h3>Toggle island</h3>
            <span class="status ok">data-island="toggle"</span>
          </header>
          <p class="muted small">This island reads its initial state from a
          <code>data-state</code> attribute. The server said <code>open: true</code>, so the
          panel is already visible on first paint and stays visible through hydration.</p>
          <label class="switch">
            <input type="checkbox" class="switch-input" role="switch"
                   data-toggle-input checked />
            <span class="switch-text">Reveal the panel</span>
          </label>
          <div class="alert alert-info" data-toggle-panel style="margin-top:1rem">
            <strong>Visible.</strong> <span>Shown only while the island's signal is true.</span>
          </div>
        </article>
      </section>

      <section class="container cta">
        <h2>Interactive where it matters. Static everywhere else.</h2>
        <div class="btn-row center">
          <a class="btn btn-secondary" href="/docs/islands">Islands docs</a>
          <a class="btn btn-secondary" href="/reactivity">Full-page reactivity</a>
        </div>
      </section>
    </main>

    <!-- Register the islands. Plain native ESM — no build step. -->
    <script type="module">
      import { registerIsland, signal, effect } from "/islands.js";

      // Counter: seed a signal from the server-printed state, wire buttons + text.
      registerIsland("counter", (el, initial) => {
        const count = signal(initial.count ?? 0);
        const step = initial.step ?? 1;

        const out = el.querySelector("#counter-value");
        effect(() => { out.textContent = String(count()); });

        el.querySelector("[data-counter-inc]").addEventListener("click", () => count(count() + step));
        el.querySelector("[data-counter-dec]").addEventListener("click", () => count(count() - step));
        el.querySelector("[data-counter-reset]").addEventListener("click", () => count(0));
      });

      // Toggle: boolean signal seeded from data-state, drives panel visibility.
      registerIsland("toggle", (el, initial) => {
        const open = signal(initial.open ?? false);

        const input = el.querySelector("[data-toggle-input]");
        const panel = el.querySelector("[data-toggle-panel]");

        input.checked = !!open();
        effect(() => { panel.style.display = open() ? "" : "none"; });
        input.addEventListener("change", () => open(input.checked));
      });
    </script>
    <script type="module" src="/islands.js"></script>
{% include "footer" %}