Thanks to visit codestin.com
Credit goes to ilha.build

Skip to content
Ilha
Esc
navigateopen⌘Jpreview

Fast pages. Less JavaScript.

Build fast websites and full apps, one island at a time.

Ilha sends ready-to-view HTML first, then adds JavaScript only to the interactive parts. Your pages load quickly, stay lightweight, and work with the stack you already use.

playground.ilha.build

Run this island in the playground.

import * as Atom from "effect/unstable/reactivity/Atom"; import { atom } from "ilha"; let nextId = 4; export default function Tasks() { const tasks = atom([ { id: 1, label: "Ship the landing page", done: true }, { id: 2, label: "Write unit tests", done: false }, { id: 3, label: "Update README", done: false }, ]); const pending = atom( Atom.map(tasks.atom, (list) => list.filter((task) => !task.done).length), ); const addItem = (event: SubmitEvent) => { event.preventDefault(); const form = event.currentTarget as HTMLFormElement; const label = String(new FormData(form).get("text") ?? "").trim(); if (!label) return; tasks.update((current) => [...current, { id: nextId++, label, done: false }]); form.reset(); }; return ( <div class="card bg-base-100 shadow"> <div class="card-body gap-3 p-3"> <h2 class="card-title text-base"> My Tasks <span class="badge badge-primary">{pending}</span> </h2> <ul class="flex flex-col gap-1"> {tasks().map((task) => ( <li key={task.id} class="flex items-center justify-between gap-2"> <label class="label cursor-pointer justify-start gap-2"> <input type="checkbox" class="checkbox" checked={task.done} onchange={(event) => { const done = event.currentTarget.checked; tasks.update((current) => current.map((item) => item.id === task.id ? { ...item, done } : item, ), ); }} /> <span>{task.label}</span> </label> <button type="button" class="btn btn-ghost btn-xs" onclick={() => tasks.update((current) => current.filter((item) => item.id !== task.id)) } > {'\u2715'} </button> </li> ))} </ul> <form onsubmit={addItem} class="flex gap-2"> <input name="text" class="input input-bordered input-sm w-full" placeholder="New task..." /> <button type="submit" class="btn btn-primary btn-sm"> Add </button> </form> </div> </div> ); }

Try this tasks island live in the playground.

Works with your existing toolsJavaScript only where neededType-safe by default

Why Ilha

Keep most of your page simple. Make only the useful parts interactive.

Build your page with familiar components. When a search box, form, or menu needs to respond to someone, turn that component into an island. Ilha leaves everything else as lightweight HTML.

Start with a function

Write a component that returns JSX. Mount it when the UI needs to live in the browser.

Grow one capability at a time

Add atom(), watch(), streams, and when only where the view needs them.

Keep your server

Any HTTP server can return Ilha HTML. Client hydration stays opt-in.

Hydrate on demand

Ship plain HTML first. Mount only the interactive regions that need JavaScript.

Easy to follow

Keep each interaction in one clear place.

The data, user actions, and HTML for a feature stay together. You can understand it at a glance, move it between pages, or remove it cleanly.

  • Familiar event handling
  • Local state with atom()
  • No app shell required

signup.tsxtsx

import { atom, mount } from "ilha"; const Signup = () => { const email = atom(""); const join = (event: SubmitEvent) => { event.preventDefault(); fetch("/api/waitlist", { method: "POST", body: JSON.stringify({ email: email() }), }); }; return ( <form class="card" onsubmit={join}> <input name="email" placeholder="[email protected]" value={email} oninput={(e) => email.set(e.currentTarget.value) } /> <button disabled={!email().includes("@")}>Join waitlist</button> </form> ); }; mount(document.getElementById("signup")!, Signup);

Efficient updates

Update only what changed.

Signals connect your data directly to the page. When something changes, Ilha updates the affected element instead of redrawing the whole interface.

  • Simple state updates
  • Stale requests cancel automatically
  • No page-wide redraws

signals.tsxtsx

import * as Effect from "effect/Effect"; import * as Stream from "effect/Stream"; import * as Atom from "effect/unstable/reactivity/Atom"; import { atom, mount, when } from "ilha"; function* Search() { const query = atom(""); yield ( <section class="card"> <input name="q" placeholder="Search…" value={query} oninput={(e) => query.set(e.currentTarget.value) } /> </section> ); yield* when( Atom.toStream(query.atom).pipe(Stream.debounce("200 millis")), function* (q) { if (!q) return undefined; const items = yield* Effect.tryPromise({ try: (signal) => fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal }).then( (r) => r.json(), ), catch: (e) => e, }); yield <ul>{(items as string[]).map((item) => <li>{item}</li>)}</ul>; return undefined; }, ); } mount(document.getElementById("search")!, Search);

Flexible delivery

Send useful content before JavaScript loads.

Render HTML on your server for a fast first view, then activate only the components people can interact with. Each island works independently.

  • Fast server-rendered HTML
  • Async data support
  • Independent interactivity

product-card.tsxtsx

import { mount, renderToString } from "ilha"; import { ProductCard } from "./product-card"; const html = await renderToString(() => ProductCard({ featured: true })); const host = document.querySelector("#product-card")!; mount(host, () => ProductCard({ featured: true }), { hydrate: true });

Add what you need

Start small. Expand when your product grows.

Begin with one lightweight package. Add routing, shared data, or Astro support only when your website needs it.

  • Pages and dynamic routes
  • Shared data for carts and sessions
  • First-class Astro integration
// vite.config.ts import { pages } from "@ilha/router/vite"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [pages()], }); // File-based routes under src/pages/ // index.tsx → / // pricing.tsx → /pricing // blog/[slug].tsx → /blog/:slug import { pageRouter } from "ilha:pages/client"; pageRouter.mount("#app", { hydrate: true });
// src/lib/cart.ts import { atom } from "ilha"; import type { Item } from "./types"; export const cart = atom<Item[]>([]); export const add = (product: Item) => cart.update((items) => [...items, product]); export const remove = (id: string) => cart.update((items) => items.filter((p) => p.id !== id)); export const count = () => cart().length;
// astro.config.ts import { defineConfig } from "astro/config"; import ilha from "@ilha/astro"; export default defineConfig({ integrations: [ilha()], });

Start your way

Add Ilha without rebuilding your stack.

Choose a starter for Vite or your server. You get a working, type-safe project with fast server-rendered pages and focused interactivity—without adopting a full application framework.

Ready to build?

Create your first interactive component in five minutes.

Follow the short guide or build a counter step by step. You will see how Ilha adds interactivity without taking over the rest of your page.