Get started · no build step
A fine-grained reactive UI library and a complete component kit — use it straight from the CDN, no install, no build step.
57 components 25 new this round
A fine-grained reactive UI library and a complete component kit — signals, a runtime html template, a router, SSR — and 57 themed, accessible, animated components. No build step: one <script type="module"> is enough.
From the CDN, or npm i @barisakin/zap. Link theme.css once.
signal() in, signal() out — every component reads and writes them.
Each component is its own module: components/rating.js, components/card.js…
How was your order?
Order #4812 · delivered today
import { signal, html } from "@barisakin/zap";import { Card } from "@barisakin/zap/components/card.js";import { Rating } from "@barisakin/zap/components/rating.js";import { Tag } from "@barisakin/zap/components/badge.js"; const stars = signal(0);const fast = signal(false); Card({ title: "How was your order?", children: html` ${Rating({ value: stars, labels })} ${Tag({ label: "Fast delivery", selected: fast })}`, footer: Button({ label: "Send", disabled: () => !stars() }),});What's in the kit
Every component, the same way
- Signals in, signals out. Pass a signal as
valueand read it anywhere; props that change take a getter. - Keyboard & screen readers. Real roles,
aria-*, focus that goes back where it came from. - Themed by tokens. Only
var(--zap-*)— dark mode and five palettes for free. - Animated, politely. Motion that explains;
prefers-reduced-motionturns it off. - Server-safe. Renders to HTML in Node for SSG / SEO, then comes alive in the browser.
- Loaded on demand. Each page pulls its component chunk the first time it opens.
⚡ This page is server-rendered. Every page of this site is prerendered in Node with linkedom via @barisakin/zap/ssr’s renderToString, then the browser hydrate()s it. View source: the markup is there before any JavaScript runs.
Under the hood
⚛️ Fine-grained reactivity
State is signals (signal / computed / effect). Reading one inside a template subscribes that exact spot; writing it re-runs only those. No virtual DOM, no diffing — a change touches the one text node or attribute that reads it.
🏷️ The html template
A tagged template parsed at runtime — no JSX, no compiler. Static markup is parsed once into a <template>; each ${hole} binds to its value. A signal in a hole tracks itself; when / forEach give keyed control flow.
🧩 Components
A component is (props) => Node — it runs once, never re-renders. Shared layers (floating.js placement, overlay.js focus and scroll lock) keep popovers, menus, pickers and dialogs behaving alike.
🧭 Routing & splitting
Real paths via the History API; the Router builds only the active page and disposes it on leave. esbuild --splitting gives each component a content-hashed chunk under dist/c/, fetched on first visit and cached for good.
🎨 Theming
Two independent axes: light / dark (a data-theme flip) and the palette (a CSS file over theme.css — Default, Emerald, Violet, Rosé, macOS Aqua). Pick them top-left.
⚙️ Build & deploy
build.sh extracts the app, code-splits it, builds the CDN zap.min.js, prerenders every route to ssg/ and stamps hashes. Static files behind nginx + Cloudflare.
Usage
<div id="app"></div> <script type="module"> import { signal, html, mount } from "https://zapjs.baltavista.com/zap.min.js"; function Counter() { const count = signal(0); return html` <button onclick=${() => count.update((c) => c - 1)}>−</button> <span>${count}</span> <button onclick=${() => count.update((c) => c + 1)}>+</button> `; } mount(Counter, document.getElementById("app"));</script>// npm i @barisakin/zapimport "@barisakin/zap/theme.css"; // the tokens every component usesimport { signal, html, mount } from "@barisakin/zap";import { Rating } from "@barisakin/zap/components/rating.js";import { TimePicker } from "@barisakin/zap/components/timepicker.js";import { FileUpload } from "@barisakin/zap/components/upload.js"; const stars = signal(4), at = signal("09:30"), files = signal([]); mount(() => html`<div class="zap"> ${Rating({ value: stars, allowHalf: true })} ${TimePicker({ value: at, hour12: true, step: 15 })} ${FileUpload({ value: files, accept: "image/*", upload })}</div>`, document.getElementById("app"));import { renderToString } from "@barisakin/zap/ssr";const html = await renderToString(App); // → server HTML string (Node, via linkedom) // client: take over the server markupimport { hydrate } from "@barisakin/zap";hydrate(App, document.getElementById("app"));