Get started · no build step

Use zap straight from the CDN — no install, no build step.

Drop one <script type="module"> in an HTML file — no install, no bundler. zap loads straight from this site's CDN.

That single ESM file (~9 KB min · ~3.7 KB gzipped) is the whole library — signals, the html template, control flow, store/context, and the router. Everything in this showcase is built with it. Prefer npm? npm i @barisakin/zap.

⚡ 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 — instant first paint & SEO, then full interactivity. View source: the markup is already there before any JavaScript runs.

import { renderToString } from "@barisakin/zap/ssr";
const html = await renderToString(App);   // → server HTML string

// client: take over the server markup
import { hydrate } from "@barisakin/zap";
hydrate(App, document.getElementById("app"));

How this site is built — the architecture in detail

⚛️ Fine-grained reactivity

State is signals (signal / computed / effect). Reading a signal inside a template or effect subscribes that exact spot; writing it re-runs only those subscribers. There is no virtual DOM and no diffing — a change mutates the single text node or attribute that reads it, never a component re-render. computed memoizes derived values, effect auto-tracks its deps, and store() makes each object key its own signal.

🏷️ The html template

A tagged template parsed at runtime — no JSX, no compiler, no build step. Static markup is parsed once into a <template>; every ${hole} becomes a comment or attribute marker bound to its value. Drop a signal straight into a hole (no () => wrapper) and it auto-tracks. when / forEach give keyed control flow that touches only the changed nodes.

🧩 Components & DI

A component is just (props) => Node — it runs once, never re-renders. Demo state lives in closures and survives navigation. createContext / provide / useContext give dependency injection without prop-drilling, and onCleanup ties teardown to the owning reactive scope.

🧭 Clean-URL routing

The History API (configureRouter({ history: true })) gives real paths — /calendar, not /#/calendar. Links intercept plain left-clicks → pushState; popstate syncs a reactive location signal; the Router builds only the active route's component and disposes it on leave. nginx try_files serves the matching prerendered file per path.

📦 Lazy code-splitting

Every component page dynamic-imports its module the first time it opens. esbuild --splitting emits one content-hashed chunk per component under dist/c/. Initial load is just the ~40 KB app shell; the grid / calendar / cropper chunks arrive on demand and cache forever (hashed names). The data-fetch resource() is lazy too.

🎨 Token-swappable theming

Components reference only var(--zap-*) tokens. Two independent axes: light/dark (a data-theme flip) and the palette (a separate CSS file layered over theme.css, swapped by changing a <link> href — Default / Emerald / Violet / Rosé / macOS Aqua). The build injects a content hash so editing a theme busts Cloudflare's edge cache.

🖥️ SSR · SSG · hydration

The same components render in NoderenderToString uses linkedom for the DOM. The build prerenders every route to its own static HTML (SSG); the browser hydrate()s — server markup paints first (fast first paint + SEO), then the live reactive tree takes over. Canvas / WebSocket pages stay client-only.

⚙️ Build & deploy

build.sh extracts the inline app, esbuild code-splits to dist/, builds the standalone zap.min.js CDN library, prerenders routes to ssg/, and stamps content hashes for cache-busting. Served as static files behind nginx (SNI-routed TLS on :8444) + Cloudflare; index.html is dynamic, every asset is hash-versioned.

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>