Live examples
Real palinx components, running right here on this page. Each recipe pairs the code with the live result. Signals drive everything: read a value in the template and only that spot updates when it changes. No virtual DOM, no dependency arrays.
Dynamic list
State is a signal holding an array. Set a new array to add or remove, and the list re-renders itself.
items = signal<string[]>([]);
add(v: string) {
// push a NEW array, which is what makes it reactive
this.items.set([...this.items(), v]);
}
remove(i: number) {
this.items.set(this.items().filter((_, x) => x !== i));
}
// template
${this.items().length
? html`<ul>${this.items().map((t, i) =>
html`<li>${t} <button @click=${() => this.remove(i)}>×</button></li>`)}</ul>`
: html`<p>No items yet.</p>`}
- Ship the docs
- Write a recipe
Reactive filter
A computed derived from a text signal. Type to filter: the visible rows and the count update as you go.
query = signal("");
matches = computed(() =>
this.all.filter(f => f.toLowerCase().includes(this.query().toLowerCase()))
);
// template: computed re-runs as you type
<input value=${this.query()}
@input=${e => this.query.set(e.target.value)} />
${this.matches().map(f => html`<li>${f}</li>`)}
- Apple
- Apricot
- Banana
- Blueberry
- Cherry
- Grape
- Mango
- Orange
- Peach
- Pear
Tabs
Conditional rendering is just a ternary in the template. The active tab is a signal; switching it swaps the panel.
tab = signal<"a" | "b" | "c">("a");
// template: a ternary IS conditional rendering
${this.tab() === "a" ? html`<p>Panel A</p>`
: this.tab() === "b" ? html`<p>Panel B</p>`
: html`<p>Panel C</p>`}
<button @click=${this.tab.set("b")}>B</button>
A batteries-included TypeScript framework for Bun: routing, signals, DI, and a self-contained build.
Live form validation
Validate as you type with a computed. The message shows on invalid input, and the button stays inert until the value is valid.
email = signal("");
valid = computed(() =>
/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(this.email())
);
// bind class + disabled state to signals (whole-attribute)
<input class=${this.showError() ? "input invalid" : "input"}
@input=${e => this.email.set(e.target.value)} />
<button class=${this.valid() ? "btn" : "btn off"}
@click=${this.submit()}>Subscribe</button>
Two-way binding + transition
A range input bound to a signal, a derived total, and a bar whose width animates through a CSS transition on reactive state.
amount = signal(40);
// two-way: input drives the signal, signal drives the view
<input type="range" value=${this.amount()}
@input=${e => this.amount.set(Number(e.target.value))} />
// whole-attribute interpolation + a CSS transition = animated
<div class="bar" style=${`width:${this.amount()}%`}></div>
<span>Doubled: ${this.amount() * 2}</span>
40
Doubled (derived): 80