zap コンポーネント:シグナルで動く小さな UI キット
3.7 KB のコア、依存関係ゼロ、57 個のコンポーネント。zap がシグナルでどう動くのか、コンポーネントをどう使うのかを簡単に紹介します。すべてのコンポーネントは deck.kartex.net でサンプル付きで動かせます。

このブログにある小さなインタラクティブ部品(言語切り替え、コードビューア、検索パレット)は zap で書かれています。zap はシグナルをベースにした、依存関係ゼロの小さな JavaScript UI ライブラリであり、それに付属するコンポーネントキットでもあります。ここでコンポーネントを一つずつ紹介することはしません。すべてのコンポーネントは、サンプルとコード付きで deck.kartex.net で実際に動かせます。この記事では、技術の仕組みと使い方を簡単にまとめるだけにとどめます。

zap の概要
- 3.7 KB のコア(gzip)。仮想 DOM なし、コンパイラなし、必須のビルドステップもありません。
- 依存関係ゼロ。 ブラウザ上で素の ES モジュールとして動きます。
- 57 個のコンポーネント:フォームと入力、オーバーレイとフィードバック、ナビゲーション、表示、データグリッド、チャート、メディア。
- SSR とハイドレーション:同じコンポーネントがサーバー上で HTML にレンダリングされ、ブラウザ上で動き出します。deck サイトの 65 ページはこの方法でプリレンダリングされています。
- テーマ:CSS 変数(
--zap-*)で、Default、Emerald、Violet、Rosé、macOS Aqua の各テーマを用意しており、それぞれにダークモードがあります。 - アクセシビリティ:すべてのコンポーネントでロール、aria 属性、フォーカス管理、キーボード操作に対応しています。
prefers-reduced-motionも尊重します。
シグナル
zap の中心にあるのはシグナルです。シグナルは値を保持する関数で、それを読み取るすべての場所が購読し、値が変わるとその場所だけが更新されます。コンポーネント全体が再描画されることはありません。
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 は読み取りを購読の対象から外し、onCleanup はエフェクトが再実行される前やコンポーネントが破棄される前に後片付けを行い、store はネストしたオブジェクトに対してフィールド単位のリアクティビティを提供します。
html テンプレート
ビューは JavaScript 標準のタグ付きテンプレートリテラルで書きます。テンプレートに関数として渡したもの(シグナルや () => ...)はすべてライブになり、そのテキストノードや属性だけが更新されます。
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 と each は条件付きの内容と繰り返しの内容を扱い、bind=${signal} は入力欄とシグナルを双方向に結び付けます。コンポーネントは (props) => Node という形の関数にすぎません。クラスも、ライフサイクルメソッドも、特別なファイル形式もありません。
コンポーネントの使い方
各コンポーネントはそれぞれ独立したモジュールになっているので、使うものだけを読み込めます。props には普通の値でもシグナルでも渡せます。シグナルを渡すと、コンポーネントはそれを読み取りもし、書き込みもします。
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!"),
}),
});
ダイアログも同じ仕組みです。open はシグナルで、true にするとダイアログが開き、ダイアログを閉じるとシグナルは 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);
ドロップダウン、ツールチップ、ポップオーバー、モーダルは二つの共通レイヤー(floating.js と overlay.js)を共有しているため、位置決め、フォーカストラップ、Escape での閉じる操作、スクロールロックは一か所で解決されています。

データとチャート
重めのものとしては、データグリッド(仮想スクロール、ソート、フィルタリング、グルーピング、編集)、スケジューラ、カンバンボード、ツリー、チャートがあります。チャートに渡すデータはシグナルにでき、それを変更するとチャートは新しい値へアニメーションしながら移ります。
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 }] });

ルーター、i18n、SSR
- ルーター:ページを再読み込みせずに遷移するための小さなルーター(
Router、Link、navigate)。 - i18n:翻訳には
t()、言語の切り替えにはsetLocale()を使います。コンポーネント自身の文言は、トルコ語、英語、ドイツ語、フランス語、スペイン語、モンテネグロ語で最初から用意されています。 - SSR:
@barisakin/zap/ssrのrenderToStringは、同じコンポーネントをサーバー上で HTML に変換します。ブラウザ側ではhydrateが既存の DOM を再利用し、イベントを結び付けます。 - TypeScript:型定義(
index.d.ts)がパッケージに同梱されています。
インストール
npm の場合:
npm install @barisakin/zap
あるいは、ツールを一切使わずに CDN から直接読み込むこともできます:
<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>
すべてを見る
すべてのコンポーネントが、ライブサンプル、コピーできるコード、テーマとダークモードの切り替え付きで deck.kartex.net にあります。あるコンポーネントの使い方を知りたいなら、そこを見るのが一番の近道です。左のメニューから選び、サンプルを試し、コードをコピーしてください。