The most versatile web UI library.
Ilha is a tiny island architecture library that renders to plain HTML on the server and hydrates on the client with zero flicker. The core is under 2,500 lines of code— small enough to paste into any AI prompt. Routing, typed forms, and shared state are included when you need them.
- < 2,500 LOC core
- Zero dependencies
- TypeScript-first
- No virtual DOM
- Star on GitHub
One island — state, events, and markup together. Edit the code and watch it run.
Build modern UI without framework ceremony.
Familiar syntax, signal-driven updates, and one island for every rendering strategy — from static HTML to hydrated interactivity.
Build interactive UI without framework ceremony
Keep state, validation, events, and markup together in one tiny island.
- Svelte-like reactivity
- React-flavored templating
- No framework ceremony
import ilha, { mount } from "ilha";
const Signup = ilha
.state("email", "")
.derived("ready", ({ state }) => state.email().includes("@"))
.on("[data-action=join]@click", async ({ state }) => {
await fetch("/api/waitlist", {
method: "POST",
body: JSON.stringify({ email: state.email() }),
});
})
.render(({ state, derived }) => (
<form class="card">
<input name="email" bind:value={state.email} placeholder="you@company.com" />
<button data-action="join" disabled={!derived.ready()}>
Join waitlist
</button>
</form>
));
mount({ Signup });Fast by default, because updates are precise
Signals track the data your UI actually reads, so updates stay focused and predictable.
- Fine-grained updates
- Abortable async work
- No app-wide re-render loop
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>
));One island, every rendering strategy
Choose the right rendering mode per island instead of committing your whole app to one strategy.
- Static HTML
- Server-rendered hydration
- Client-only islands
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 mount client-side when SEO is not required.
mount({ ProductCard });Routing and shared state when you need them
Start with a single import and add file-based routes or Zustand-shaped stores only when the product earns it.
- File-based routes and dynamic pages
- Shared cart and session state
- Cross-island coordination
// 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";
pageRouter.start();Pick a template, optional project name, and copy the giget command — or open a StackBlitz sandbox.
