palinxdocs

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.

Code
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>`}
Live
  • Ship the docs
  • Write a recipe
2 items

Reactive filter

A computed derived from a text signal. Type to filter: the visible rows and the count update as you go.

Code
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>`)}
Live
  • Apple
  • Apricot
  • Banana
  • Blueberry
  • Cherry
  • Grape
  • Mango
  • Orange
  • Peach
  • Pear
10 of 10

Tabs

Conditional rendering is just a ternary in the template. The active tab is a signal; switching it swaps the panel.

Code
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>
Live

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.

Code
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>
Live

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.

Code
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>
Live

40

Doubled (derived): 80