BA Barış Akın~/blog

·3 min read·47 views

zap Components: A Small UI Kit Driven by Signals

A 3.7 KB core, zero dependencies, 57 components: a short look at how zap works with signals and how the components are used. All of them are live, with examples, at deck.kartex.net.

zap Components: A Small UI Kit Driven by Signals

The small interactive pieces of this blog (the language switcher, the code viewer, the search palette) are written with zap. zap is a tiny, dependency-free JavaScript UI library built on signals, and a component kit that ships with it. I won't go through the components one by one here; all of them are live, with examples and code, at deck.kartex.net. This post is just a short summary of how the technology works and how you use it.

deck.kartex.net: a feedback card and the code that builds it

zap in short

  • 3.7 KB core (gzip). No virtual DOM, no compiler, no mandatory build step.
  • Zero dependencies. It runs in the browser as plain ES modules.
  • 57 components: forms and inputs, overlays and feedback, navigation, display, data grids, charts, media.
  • SSR and hydration: the same components render to HTML on the server and come alive in the browser. The 65 pages of the deck site are prerendered this way.
  • Themes: CSS variables (--zap-*) with Default, Emerald, Violet, Rosé and macOS Aqua themes, each with a dark mode.
  • Accessibility: roles, aria attributes, focus management and keyboard use in every component; prefers-reduced-motion is respected.

Signals

The heart of zap is the signal. A signal is a function that holds a value; every place that reads it subscribes, and when the value changes only those places update. The component as a whole is never redrawn.

import { signal, computed, effect } from "@barisakin/zap";

const count = signal(0);
const double = computed(() => count() * 2);

effect(() => console.log("count", count(), "double", double()));

count.set(1);              // the effect runs again
count.update(n => n + 1);  // 2

untrack keeps a read out of the subscription, onCleanup tidies up before an effect reruns or a component goes away, and store gives field-level reactivity for nested objects.

html templates

Views are written with JavaScript's own tagged template literals. Anything passed into the template as a function (a signal or () => ...) is live; only that text node or attribute updates.

import { signal, html, mount } from "@barisakin/zap";

function Counter() {
  const n = signal(0);
  return html`
    <button onclick=${() => n.update(x => x + 1)}>
      clicked ${n} times
    </button>
  `;
}

mount(Counter, document.getElementById("app"));

when and each cover conditional and repeated content, and bind=${signal} ties an input to a signal both ways. A component is nothing more than a function of the form (props) => Node: no classes, no lifecycle methods, no special file format.

Using the components

Every component lives in its own module, so you only load what you use. Props can be plain values or signals; give a signal and the component both reads and writes it.

import { signal, html } from "@barisakin/zap";
import { Card } from "@barisakin/zap/components/card.js";
import { Rating } from "@barisakin/zap/components/rating.js";
import { Button } from "@barisakin/zap/components/button.js";
import { toast } from "@barisakin/zap/components/toast.js";

const stars = signal(0);

Card({
  title: "How was your order?",
  children: html`${Rating({ value: stars })}`,
  footer: Button({
    label: "Send",
    disabled: () => stars() === 0,
    onClick: () => toast.success("Thank you!"),
  }),
});

A dialog works the same way: open is a signal, set it to true and the dialog opens, close it and the signal goes back to false.

import { Modal } from "@barisakin/zap/components/modal.js";

const open = signal(false);
Modal({ open, title: "Delete it?", children: "This cannot be undone." });
open.set(true);

Dropdowns, tooltips, popovers and modals share two common layers (floating.js and overlay.js), so positioning, focus trapping, closing on Escape and scroll locking are solved in one place.

Kanban: drag and drop, and keyboard moves

Data and charts

On the heavier side there is a data grid (virtual scrolling, sorting, filtering, grouping, editing), a scheduler, a kanban board, a tree and charts. The data you give a chart can be a signal; change it and the chart animates to the new values.

import { ColumnChart } from "@barisakin/zap/components/chart.js";

const data = signal([12, 19, 7, 24]);
ColumnChart({ labels: ["Q1", "Q2", "Q3", "Q4"], series: [{ name: "Sales", data }] });

Charts: line, area and bar

Router, i18n and SSR

  • Router: a small router for navigating without page reloads (Router, Link, navigate).
  • i18n: t() for translations, setLocale() to switch languages; the components' own strings come ready in Turkish, English, German, French, Spanish and Montenegrin.
  • SSR: renderToString from @barisakin/zap/ssr turns the same component into HTML on the server; in the browser hydrate reuses the existing DOM and wires up the events.
  • TypeScript: type definitions (index.d.ts) ship with the package.

Installing

With npm:

npm install @barisakin/zap

or straight from the CDN, with no tooling at all:

<link rel="stylesheet" href="https://zapjs.baltavista.com/theme.css">
<script type="module">
  import { signal, html, mount } from "https://zapjs.baltavista.com/zap.min.js";
</script>

Seeing them all

Every component, each with a live example, copyable code, and theme and dark mode switches, is at deck.kartex.net. If you want to know how to use a component, the quickest way is to look there: pick it from the left menu, try the example, copy the code.

BA
Barış Akın

Software solutions architect with 25+ years of experience. FinTech and distributed systems; creator of OxiDB, Rill and Cobra.