Your server renders the page.
Aegis makes parts of it alive.
One ES module, no build step, no npm. Django, Laravel, Rails, Go, PHP: keep your templates, add data-aegis where the page has to react.
Pinned, with an integrity hash: the import map. v0.7.0 · MIT · when not to use it
<div data-aegis="counter" data-start="5">
<button>Clicked 5 times</button>
</div>
<script type="module">
import { island } from 'https://aegisjs.com/0.7.0/aegis.min.js';
island('counter', ({ props, signal, html }) => {
const count = signal(props.start);
return html`<button @click=${() => count.value++}>
Clicked ${count} times
</button>`;
}, { types: { start: Number } });
// no mount() call: every [data-aegis] on the page is hydrated after island()
</script>
Where are you coming from?
Four doors. Each one is two minutes to your own page, not a tour of the engine.
Four rules, no surprises
The whole mental model fits on a card.
${x.value}is a snapshot,${x}and${() => …}are live. A signal or a function in a template re-renders on change; a plain value renders once (dev warns with E019).- Everything lives in a scope. Effects, listeners, timers and resources created inside
island()/mount()die with the component. Outside a scope you get E001. - Use the
ctxversions.on,effect,interval,observefrom the setup context are bound to the component's scope; the imported ones are not. - Data never goes through
innerHTML. Data goes through${}inhtml``(text nodes, never parsed). Server HTML goes throughswap()/adopt().
Three jobs it does on your page
The server keeps rendering. Aegis takes the parts that have to react.
A live piece on a server page
<div data-aegis="likes" data-count="12"> is rendered by the server, visible at once, indexed; the island replaces it with a live template and keeps working after any swap.
A form that works without JavaScript
Your <form> submits as it always did. wireForm() adds live validation from the browser's own rules and lands a 422 on the right fields.
A list or table with search and cache
resource() turns a query signal into requests that abort, dedupe, cache and revalidate; list() keeps rows keyed and moves only what changed.
Also in the same file, when a page needs it: a router · swap and morph for server fragments · offline and optimistic updates · springs and FLIP · focus traps and live regions · typed attribute sinks. All of it in the API reference.
Works with what you have
No npm required. The same island file for Django, Rails, Laravel, Go and PHP; markup inserted by htmx, Turbo or jQuery is hydrated too.
import { island, mutation, api, configure } from 'aegis';
configure({ csrf: 'django' }); // or 'laravel', 'rails', { header, cookie }
island('likes', ({ props, signal, html }) => {
const count = signal(props.count);
const like = mutation(() => api.post(`/api/products/${props.id}/like`), {
optimistic: () => { count.value++; },
onError: () => { count.value--; },
});
return html`<button @click=${like} ?disabled=${like.pending}>${count} likes</button>`;
}, { types: { id: Number, count: Number } });
Ten names
Aegis exports more, but these ten are the core: stable through 0.x, enough for most pages. The rest is for specific jobs.
Numbers, with the neighbours
A 1 000-row table is created in 17.1 ms (median of 5, headless Chrome, bench.html). Numbers without a neighbour are a poster, so the site has a second page that runs the same three jobs in your own browser next to Alpine and Vue: a 10 000-row table, a search filter, an optimistic PATCH.
Add one script tag. Ship.
No npm, no bundler, no config. A pinned URL with a hash, on the pages your server already renders.