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.
import ilha from "ilha";
import { Button, Checkbox, Input, LayerCard } from "areia";
let nextId = 4;
export default ilha
.state("tasks", [
{ id: 1, label: "Ship the landing page", done: true },
{ id: 2, label: "Write unit tests", done: false },
{ id: 3, label: "Update README", done: false },
])
.state("draft", "")
.derived("pending", ({ state }) =>
state.tasks().filter((task) => !task.done)
)
.action("add", (event: SubmitEvent, { state }) => {
event.preventDefault();
const label = state.draft().trim();
if (!label) return;
state.tasks((tasks) => [
...tasks,
{ id: nextId++, label, done: false },
]);
state.draft("");
})
.action("remove", (id: number, { state }) => {
state.tasks((tasks) =>
tasks.filter((task) => task.id !== id)
);
})
.render(({ state, derived, action }) => (
<LayerCard>
<LayerCard.Title>
My Tasks ({derived.pending().length})
</LayerCard.Title>
<LayerCard.Content class="p-0">
<ul class="divide-y divide-areia-border">
{state.tasks().map((task, index) => (
<li
key={task.id}
class="flex items-center gap-2 p-2"
>
<div class="flex-1">
<Checkbox
bind:checked={state.tasks.select(
(tasks) => tasks[index].done
)}
label={task.label}
/>
</div>
<Button
onclick={() => action.remove(task.id)}
size="sm"
>
✕
</Button>
</li>
))}
</ul>
<form
onsubmit={action.add}
class="flex gap-2 border-t border-areia-border p-2"
>
<Input
placeholder="New task…"
bind:value={state.draft}
class="flex-1"
/>
<Button type="submit" disabled={!state.draft()}>
Add
</Button>
</form>
</LayerCard.Content>
</LayerCard>
));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.
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
- Type-safe actions
- No app shell required
import ilha, { mount } from "ilha";
const Signup = ilha
.state("email", "")
.derived("ready", ({ state }) =>
state.email().includes("@")
)
.action("join", async (event: SubmitEvent, { state }) => {
event.preventDefault();
await fetch("/api/waitlist", {
method: "POST",
body: JSON.stringify({ email: state.email() }),
});
})
.render(({ state, derived, action }) => (
<form class="card" onsubmit={action.join}>
<input
name="email"
bind:value={state.email}
placeholder="you@company.com"
/>
<button disabled={!derived.ready()}>
{action.join.pending ? "Joining…" : "Join waitlist"}
</button>
</form>
));
// Hydrate matching server-rendered Signup hosts.
mount({ 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
import ilha from "ilha";
const Search = ilha
.state("query", "")
.derived("results", async ({ state, signal }) => {
if (!state.query()) return [];
const res = await fetch(
`/api/search?q=${encodeURIComponent(state.query())}`,
{ signal }
);
return res.json() as Promise<string[]>;
})
.render(({ state, derived }) => (
<section class="card">
<input
name="q"
placeholder="Search…"
bind:value={state.query}
/>
<Results items={derived.results() ?? []} />
</section>
));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
import { mount } from "ilha";
import { ProductCard } from "./product-card";
// Static HTML — instant first paint.
const html = ProductCard.toString({ featured: true });
// Hydrate only where you need interactivity.
const island = await ProductCard.hydratable(
{ featured: true },
{ name: "ProductCard", snapshot: true },
);
// Or render directly into a client-side host.
const host = document.querySelector("#product-card")!;
ProductCard.mount(host, { featured: 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, registry } from "ilha:pages/client";
pageRouter.hydrate(registry);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.