Frontend example
A todo list using Para against the vanilla DOM, built with Vite. Demonstrates signal, derived signals, effect { }, the ~> reactive-binding operator, and the bare-read sugar.
Project layout
Section titled “Project layout”my-todo/├── index.html├── src/main.pjs├── vite.config.js└── package.jsonindex.html
Section titled “index.html”<!doctype html><html> <body> <input id="new" placeholder="new todo…" autofocus /> <button id="add">add</button> <select id="filter"> <option value="all">all</option> <option value="open">open</option> <option value="done">done</option> </select> <ul id="list"></ul> <p id="count"></p> <p id="toast" hidden>all done</p> <script type="module" src="./src/main.pjs"></script> </body></html>src/main.pjs
Section titled “src/main.pjs”signal items = [];signal filter = "all";
derived visible = filter === "all" ? items : items.filter(t => (filter === "done" ? t.done : !t.done));derived openCount = items.filter(t => !t.done).length;
let nextId = 1;
const $ = document.querySelector.bind(document);const input = $("#new");const addBtn = $("#add");const filterEl = $("#filter");const list = $("#list");const count = $("#count");const toast = $("#toast");
addBtn.addEventListener("click", () => { if (!input.value.trim()) return; items = [...items, { id: nextId++, text: input.value.trim(), done: false }]; input.value = "";});
filterEl.addEventListener("change", () => { filter = filterEl.value;});
// Effect for the list — long body, multiple template lines. Effect// blocks are the right shape when the body isn't a single sink.effect { list.innerHTML = visible .map(t => ` <li data-id="${t.id}"> <input type="checkbox" ${t.done ? "checked" : ""} /> <span class="${t.done ? "done" : ""}">${t.text}</span> </li> `).join("");}
// Single-sink reactive bindings. Each `A ~> B` desugars to// effect(() => { B = A; }). One per line keeps the dep set tight —// the textContent binding only re-fires on openCount changes.`${openCount} open` ~> count.textContent;!openCount && !items.length ~> count.hidden;!items.length || openCount > 0 ~> toast.hidden;
list.addEventListener("click", e => { if (e.target.tagName !== "INPUT") return; const li = e.target.closest("li[data-id]"); if (!li) return; const id = +li.dataset.id; items = items.map(t => (t.id === id ? { ...t, done: !t.done } : t));});Notes on the source
Section titled “Notes on the source”signal items = []declares a reactive cell.items = newValuecompiles toitems.set(newValue); readingitemsinside a tracked context (aneffect, aderived, awhenpredicate, or another signal’s RHS) compiles toitems.get().derived visible = ...andderived openCount = ...are read-only signals computed fromitems/filter. They recompute lazily when a dep changes. (You can writesignal NAME = …instead and the transpiler auto-promotes when the RHS reads other signals — butderivedmakes the read-only intent explicit at the declaration site.)- Three reactive shapes, picked by what the body does.
effect { … }is the multi-statement form (the list-render block tracksvisibleand re-runs the whole template).A ~> Bis the single-sink form (the three lines at the bottom each bind one expression to one DOM property — keeping them per-line is what makescount.textContentre-fire only onopenCountchanges, not on unrelateditems.lengthreads).when EXPR { … }(paired withwhen stop { … }) is the edge-triggered form for transitions — playing a sound, sending analytics, anything that should fire on the change rather than every render. - The
$shorthand bindsdocument.querySelectoronce.
vite.config.js
Section titled “vite.config.js”No Para-specific config needed. The transpiler emits standard import "@para/signals" statements that Vite resolves through node_modules:
import { defineConfig } from "vite";export default defineConfig({});package.json
Section titled “package.json”{ "name": "my-todo", "type": "module", "scripts": { "build": "parabun build src/main.pjs --outfile dist/main.js && vite build", "dev": "parabun build src/main.pjs --watch --outfile dist/main.js & vite" }, "dependencies": { "@para/signals": "*", "@para/parallel": "*" }}Build and deploy
Section titled “Build and deploy”parabun installparabun run buildOutput is a static dist/ directory. Deploy to any static host (Cloudflare Pages, Vercel, S3, GitHub Pages, etc.).
TypeScript form
Section titled “TypeScript form”If you prefer types, swap src/main.pjs for src/main.pts, type the signals (signal items: Todo[] = []), and update index.html and package.json to point at .pts. Same compiled output either way.