Examples

25 patterns, each on its own page with an editable app.js and index.html, a live result on a mock server, the server requests it makes and what to notice. Change the code and it re-runs.

Islands on server HTML

Forms

Navigation & server HTML

Lists & tables

Data & cache

Basics

Motion & accessibility

Single-file recipes

The same idea as one self-contained HTML file each, straight from the repository: read the source, open it in a new tab, or continue in the playground.

01Island on a server pageisland(), typed data-* props, mutation with optimistic update
island.html · 17 lines
<!-- rendered by the server: visible before JS loads, replaced by the island's template after hydration -->
<ul data-aegis="likes" data-url="/api/likes" data-count="12">
    <li>12 likes</li>
</ul>

<script type="module">
import { island, mutation, defaults } from '../aegis.js';

defaults.fetcher = async () => ({ ok: true });   // demo: no server behind /api/likes

island('likes', ({ props, signal, html }) => {
    const count = signal(props.count);                     // data-count="12" → 12 (types)
    const like = mutation(() => defaults.fetcher(props.url), { optimistic: () => count.value++ });
    return html`<li>${count} likes <button @click=${like} ?disabled=${like.pending}>♥</button></li>`;
}, { types: { count: Number } });
// no mount(): every [data-aegis] on the page is hydrated after island()
</script>
Result
03Progressive formwireForm() over a plain <form>: validation, a mock server error, submit state
form.html · 27 lines
<!-- a normal server form: works without JS; the island adds live validation on top -->
<form data-aegis="signup" method="post" action="/signup">
    <label>Email <input name="email" type="email" required></label>
    <label>Password <input name="password" type="password" required minlength="8"></label>
    <label>Repeat <input name="repeat" type="password" required></label>
    <button>Create account</button>
</form>

<script type="module">
import { island, wireForm, required, emailRule, minLen, matches } from '../aegis.js';

island('signup', ({ el, html }) => {
    const f = wireForm(el, {
        schema: {
            email: [required, emailRule],
            password: [required, minLen(8)],
            repeat: [required, matches('password', 'Passwords differ')],
        },
        mode: 'blur-then-live',   // quiet until the field is left, live after that
        // mock server (try me@taken.com): a real app passes submit: true and lets the 422 → field errors path do this
        submit: async (v) => { await new Promise(r => setTimeout(r, 500)); if (v.email.endsWith('@taken.com')) { f.setErrors({ email: 'This email is already registered' }); throw new Error('422'); } },
    });
    // f.errors.email, f.valid, f.submitting are signals; errors are also written into the DOM (aria-invalid, :user-invalid)
    el.insertAdjacentElement('afterend', html`<p aria-live="polite">${() => f.submitting.value ? 'Creating…' : f.status.value === 'success' ? 'Account created (mock)' : ''}</p>`.firstElementChild);
});
</script>
<style>input:user-invalid { outline: 2px solid #C8353B }</style>
Result
05Sortable, filterable tablereactive() + list() keyed rows, computed sort
table.html · 25 lines
<div id="app"></div>

<script type="module">
import { mount, reactive } from '../aegis.js';

const ROWS = Array.from({ length: 200 }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}`, price: (i * 37) % 500, stock: i % 7 }));

mount('#app', ({ html, list, computed, signal }) => {
    const state = reactive({ rows: ROWS, q: '', sortBy: 'id', dir: 1 });
    // getters on reactive() are computeds; array methods track one dependency per call
    const view = computed(() => state.rows
        .filter(r => r.name.toLowerCase().includes(state.q.toLowerCase()))
        .toSorted((a, b) => (a[state.sortBy] > b[state.sortBy] ? 1 : -1) * state.dir));
    const sort = (key) => { if (state.sortBy === key) state.dir *= -1; else { state.sortBy = key; state.dir = 1; } };
    const th = (key, label) => html`<th @click=${() => sort(key)} class=${{ active: () => state.sortBy === key }}>${label}</th>`;
    return html`
        <input placeholder="Filter…" @input=${(e) => { state.q = e.target.value; }}>
        <span>${() => view.value.length} rows</span>
        <table>
            <thead><tr>${th('id', '#')}${th('name', 'Name')}${th('price', 'Price')}${th('stock', 'Stock')}</tr></thead>
            <tbody>${list(view, (r) => html`<tr class=${{ low: r.stock === 0 }}><td>${r.id}</td><td>${r.name}</td><td>${r.price}</td><td>${r.stock}</td></tr>`, { key: 'id' })}</tbody>
        </table>`;
});
</script>
<style>th { cursor: pointer } th.active { text-decoration: underline } tr.low { color: crimson }</style>
Result

Bigger things

Admin app

Hash router, table with search and paging, optimistic mutations, forms with server errors, a 50 000-line virtual log, offline settings, theme and i18n on a mock server. One file.

Open the demo →

Benchmark

A js-framework-benchmark-style table (1 000 rows, list() + html``) plus the reactive core. Numbers land in a <pre> and window.__bench.

Run it in your browser →

DevTools

The in-page inspector is itself an Aegis app: component tree, signals with change marks, effects and their dependencies, cache tab. Add ?aegis-devtools to any page.

Admin demo with DevTools →