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.

Core, gzipped
3.7 KB
Components
57
Dependencies
0
Pages prerendered
65
1
Add it

From the CDN, or npm i @barisakin/zap. Link theme.css once.

2
Hold state in signals

signal() in, signal() out — every component reads and writes them.

3
Import what you use

Each component is its own module: components/rating.js, components/card.js

How was your order?

Order #4812 · delivered today

Tap a star
Nothing picked yet
JavaScriptfeedback.js
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 value and 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-motion turns 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

HTML
<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>