@kitwork/kitjs
v1.0.0
Published
A CDN-first, no-build browser runtime for scoped reactive HTML and optional navigation continuity.
Maintainers
Readme
KitJS
Reactive HTML from one browser script.
KitJS adds local state, bindings, events, conditional rendering, lists, and
small components to ordinary HTML through data-kit-* attributes. It is a
standalone classic script: no frontend build, virtual DOM, package loader, Go
runtime, or Kitwork server is required.
Stable release build: this checkout and its generated browser artifacts identify as
1.0.0. Public npm/CDN availability and npmlatestresolution are recorded only after independent retrieval; see release evidence. Pin the exact version and SRI shown below for production.
@kitwork/kitjs is the standalone browser package. Kitwork JITJS—
router.jitjs(...), /jit/... delivery, managed component/service graphs, and
data-kit-action—is server-side Kitwork functionality and is not a KitJS
package API.
Start with one CDN script
For a quick experiment, the bare jsDelivr entry follows npm latest and may
be automatically minified by jsDelivr:
<script defer src="https://cdn.jsdelivr.net/npm/@kitwork/kitjs"></script>That URL upgrades automatically and its bytes may change. Do not attach the
dist/kit.js SRI to it. Production pages should pin the exact readable file
and its matching SRI as shown next.
Use the Kit profile when links and forms should use normal browser navigation:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>KitJS counter</title>
<script
defer
src="https://cdn.jsdelivr.net/npm/@kitwork/[email protected]/dist/kit.js"
integrity="sha256-LXt1DK4QG43susUNwzTQp7H046G1/gONdOhBAcXVIZI="
crossorigin="anonymous"></script>
</head>
<body>
<section data-kit-scope="count: 0">
<button type="button" data-kit-click="count--">−</button>
<output data-kit-text="count">0</output>
<button type="button" data-kit-click="count++">+</button>
</section>
</body>
</html>That is enough for local reactive UI. data-kit-scope creates a shallow state
boundary, descendant directives use the nearest boundary, and synchronous
writes are batched into one render. No component registration is needed.
Choose one profile
KitJS publishes two complete, mutually exclusive profiles:
| Profile | Exact file | Navigation |
|---|---|---|
| Kit | dist/kit.js | Normal browser document navigation |
| Hydrate | dist/hydrate.kit.js | Optional compatibility-checked Drive/Morph, with native fallback |
Use Hydrate only when you want eligible same-origin links and GET forms to continue without replacing the live document:
<script
defer
src="https://cdn.jsdelivr.net/npm/@kitwork/[email protected]/dist/hydrate.kit.js"
integrity="sha256-AbI+PkXOVgSxZDNiMjQCxehrcASWQuW+mJHrSmfS57k="
crossorigin="anonymous"></script>Hydrate already contains the complete Kit runtime. Never load both profiles on one page. When a Hydrate destination is incompatible, the browser performs a normal navigation before KitJS mutates the current document.
Scopes and directives
A scope contains data, not arbitrary JavaScript behavior:
<section data-kit-scope="count: 3; open: true; profile: { name: 'Ada' }">
<button type="button" data-kit-click="open = !open">Toggle</button>
<p data-kit-show="open">
<strong data-kit-text="profile.name">Ada</strong>
clicked <output data-kit-text="count">3</output> times.
</p>
</section>The object form is also supported:
<section data-kit-scope="{ count: 3, open: true }"></section>Top-level state names are ASCII identifiers. Values may contain null,
booleans, finite numbers, strings, arrays, and plain objects. Scope declarations
do not accept calls, property reads, lambdas, or executable initialization
programs.
The main authored directives are:
| Purpose | Example |
|---|---|
| Local state | data-kit-scope="count: 0; open: true" |
| Text | data-kit-text="user?.name ?? 'Guest'" |
| Visibility | data-kit-show="open" |
| Safe attributes | data-kit-bind="aria-expanded: open; disabled: busy" |
| Classes | data-kit-class="open ? 'block opacity-100' : 'hidden opacity-0'" |
| Continuous styles | data-kit-style="width: progress + '%'; opacity: visible ? 1 : 0" |
| Form state | data-kit-model="name" |
| Events | data-kit-click:prevent="save()" |
| Conditional element | <p data-kit-if="ready">Ready</p> |
| Conditional fragment | <template data-kit-if="ready">...</template> |
| Keyed list | <template data-kit-for="item, i of items" data-kit-key="item.id">...</template> |
| Ownership opt-out | data-kit-ignore |
Unknown directives and invalid expressions fail closed.
if, show, and for
Use data-kit-if directly on an ordinary element for a single branch:
<section data-kit-scope="ready: false">
<button type="button" data-kit-click="ready = !ready">Toggle</button>
<p data-kit-if="ready">Ready.</p>
</section>Use a template when the branch needs multiple top-level nodes, no wrapper, or content that must remain inert until materialized:
<template data-kit-if="ready">
<h2>Ready</h2>
<p>The complete fragment mounts together.</p>
</template>data-kit-for and data-kit-key remain template-only. data-kit-show keeps a
node mounted and changes visibility; data-kit-if disposes the branch when it
becomes false. An initially true direct branch keeps the exact authored host.
After it unmounts, a later true value creates fresh node and component identity.
An ordinary element with data-kit-if is normal browser DOM before KitJS
prepares it, so it may paint or start resources first. Client-side conditions
are not authorization, secrecy, inertness, or no-flash boundaries. Scripts are
forbidden in structural branches; keep secrets and authorization decisions out
of client-authored HTML.
Events and expressions
Event attributes use the native event name:
<form data-kit-submit:prevent="save()">
<input data-kit-keydown:escape="close()">
<button data-kit-click:once="count++">Run once</button>
<button data-kit-click:debounce(250)="search()">Search</button>
</form>Supported events are click, dblclick, submit, input, change,
keydown, keyup, pointerdown, pointerup, focusin, and focusout.
Modifiers are self, prevent, stop, once, outside, enter, escape,
and debounce(ms).
Bindings are read-only. Actions may assign or apply ++/-- to an existing
writable top-level field. Multiple writes commit together only when the whole
synchronous action succeeds.
The expression language supports literals, arrays and objects, arithmetic, comparisons, logical and nullish operators, conditionals, safe method calls, expression lambdas, and continuous optional chains. It is deliberately not JavaScript: declarations, loops, constructors, template literals, member assignment, browser globals, and prototype escape names are unavailable.
Standalone components
Use a component only when a boundary needs trusted methods or lifecycle. CDN
package components are registered directly with an unversioned name; the
general host form is data-kit-component="name":
<section data-kit-component="counter">
<button type="button" data-kit-click="increment()">Increment</button>
<output data-kit-text="count">0</output>
</section>
<script
defer
src="https://cdn.jsdelivr.net/npm/@kitwork/[email protected]/dist/kit.js"
integrity="sha256-LXt1DK4QG43susUNwzTQp7H046G1/gONdOhBAcXVIZI="
crossorigin="anonymous"></script>
<script defer src="/assets/components.js"></script>// /assets/components.js
kit.component("counter", {
count: 0,
increment() {
this.count += 1;
}
});Each host receives an isolated shallow instance. A data-kit-scope on the same
host may seed existing writable data fields, but it cannot add fields or replace
methods. data-kit-as="$name" gives the component an action-only alias.
If a host names a component that has not been registered, KitJS reports the missing definition once and leaves the authored fallback DOM in place. It does not fetch component code and it does not reload the page. Register definitions through the ordered script shown above; a later explicit registration can still mount matching connected hosts.
The optional trusted init(context) hook receives a frozen context:
| Member | Purpose |
|---|---|
| host | Current component host, then null after disposal |
| owned(selector) | Fresh matches owned by this boundary |
| listen(target, type, fn, options) | Native listener with automatic cleanup |
| cleanup(fn) | Register an idempotent disposer |
| afterRender(fn) | Run once after this boundary's next render |
Simple components do not need init. Trusted component JavaScript has normal
page authority and must never be built from untrusted source.
The complete public JavaScript API is:
kit.version
kit.component(name, plainObject)globalThis.kit is frozen. There is no public compiler, renderer, manual
mount/destroy control, plugin loader, navigation object, or service registry.
data-kit-version is unsupported, and there is no data-kit-local marker.
Hydrate and Drive
Drive enhances compatible same-origin links and GET forms. It does not require a special router and it never executes scripts discovered in fetched HTML.
For a route group to remain compatible:
- the Hydrate script must be a classic external
deferscript and a direct child ofhead; - every page must preserve its resolved URL, position, order, and complete attributes;
- every other executable script that must persist must follow the same external direct-head topology;
- cross-origin scripts, including jsDelivr, must carry valid SRI and must not
use
data-kit-drive="stable".
For a self-hosted same-origin external script, data-kit-drive="stable" may be
used as an author promise that its URL, bytes, position, order, and attributes
will remain unchanged:
<script defer src="/assets/hydrate.kit.js" data-kit-drive="stable"></script>The marker is not a content check. Prefer a versioned or content-hashed URL with
SRI for production. Inline scripts, body scripts, modules, import maps,
speculation rules, async, nomodule, or any executable topology change make
the destination incompatible. The result is supported native navigation, not a
partially applied Morph.
An inline kit.component(...) registration is therefore fine with the Kit
profile, but it makes Hydrate leave navigation to the browser. To preserve
Drive eligibility, put component registrations in one external file whose
exact tag is shared by every compatible route.
The same rule applies to any executable inline script, including a simple
<script>console.log("hello world")</script>. Hydrate does not reload because
of the script itself; it declines Drive, and the next navigation becomes a
normal full document load where the browser executes that script.
Use data-kit-drive="false" on a link, GET form, submitter, or ancestor when a
route must always navigate normally.
Hydrate validates the initial script topology before intercepting navigation. An invalid initial page leaves links and forms native and emits one diagnostic. An incompatible fetched destination falls back before mutating title, head, history, or body. Responses over 8 MiB, parsed documents over 100,000 nodes, or documents deeper than 256 also fall back natively.
Same-document fragment links remain browser-native. Compatible cross-route visits preserve requested fragments, focus, scroll, forms, Back/Forward state, and component cleanup according to the browser contract.
Ownership during Morph
data-kit-ignoreleaves a host and its subtree inert to KitJS and opaque to Morph. It is an ownership marker, not a sanitizer.data-kit-retain="key"preserves a registered standalone component host and its live state across compatible Hydrate navigation. Keys must be unique and stable; retained components cannot live inside structural branches.
Security model
- Authored expressions never execute through
eval()orFunction. - Expressions cannot access
window,document, the native event object, or the trustedkitobject. data-kit-bindblocks event handlers, raw style,srcdoc, HTML replacement sinks, unsafe URL schemes, anddata-kit-*targets.data-kit-stylevalidates the complete property map before writing.- Hydrate never executes scripts from fetched HTML; incompatible pages navigate through the browser document loader.
- Component definitions are trusted application JavaScript and retain normal browser authority.
KitJS narrows the authority of authored expressions; it is not a general HTML sanitizer and cannot make an unsafe application safe by itself.
Release artifact identity
The deterministic 1.0.0 build in this checkout produces these exact readable
profile files:
| Profile | Bytes | SHA-256 | SRI |
|---|---:|---|---|
| Kit | 206,607 | 2d7b750cae101b8decbac50dc334d0a7b1f4e3a1b5fe038d74e84101c5d52192 | sha256-LXt1DK4QG43susUNwzTQp7H046G1/gONdOhBAcXVIZI= |
| Hydrate | 314,424 | 01b23e3e45ce5604b1643362323402c5e86b70049642e5be9891eb4a67d2e7b9 | sha256-AbI+PkXOVgSxZDNiMjQCxehrcASWQuW+mJHrSmfS57k= |
These local identities do not by themselves claim public availability. The
immutable 1.0.0-rc.2 publication evidence remains in
RELEASE_READINESS.md until the stable tag, package,
CDN files, signatures, and provenance are independently recorded.
1.0.0 includes direct ordinary-element data-kit-if, first published in
1.0.0-rc.2. The immutable 1.0.0-rc.1 artifact still requires
<template data-kit-if>.
More documentation
- Browser contract — exact standalone syntax and behavior
- Vietnamese guide — concise package guide in Vietnamese
- Static deployment — CDN, self-hosting, SRI, CSP, and caching
- Static Hydrate example — advanced Drive topology
- Support policy — release and browser evidence
- Security policy — private vulnerability reporting
- Building and releasing — maintainer-only source and release workflow
- Release readiness — hashes, CI, provenance, and publication evidence
- Roadmap — compatibility and tooling direction
License
MIT © 2026 Huỳnh Nhân Quốc.
