Composable JavaScript Framework for building reactive web interfaces with declarative JavaScript.
Quick Start • Why Jetz? • 60 Seconds • Core Concepts • App Features • Calculator Example • API Reference
Build modern, reactive web interfaces using clean JavaScript composition — without JSX, build-time compilation flags, or complex template languages.
Inspired by the composable, declarative paradigm of Jetpack Compose, Jetz brings that elegance directly to web developers using native JavaScript functions and direct, lightweight DOM updates.
import { Jetz, stateOf } from "@daevsoft/jetz";
import { button, div, p } from "@daevsoft/jetz/ui";
const count = stateOf(0);
const App = () => div(
p("Count: ", count),
button({ onclick() { count.value++; } }, "Increment")
);
Jetz.mount(App, "#app");pnpm add @daevsoft/jetz
# or
npm install @daevsoft/jetz- Why Jetz?
- Jetz in 60 Seconds
- Feature Overview
- Thinking in Jetz
- Jetz vs Traditional DOM
- Why No JSX or Templates?
- When Should I Use Jetz?
- Quick Start
- Learning Path
- Core Concepts
- 1. Elements & UI Composition
- 2. Components (Functions & Classes)
- 3. Reactive State (
stateOf,rememberOf) - 4. Computed State (
computed) - 5. Side-Effects (
effect) - 6. Reactive Collections (
listOf,sequenceOf) - 7. Keyed List Reconciliation (
loop) - 8. Conditional Rendering (
_if,_elseif,_else,ifElse) - 9. Component Lifecycle
- 10. Two-Way Data Binding (
bind) - 11. Reactive Listeners (
listen) - 12. DOM Utilities & Helper Methods
- From Small UI to Complete Application
- Application Features
- Real-World Example: Calculator
- Architecture Overview
- API Quick Reference
- Testing
- Development & Build
- Publishing
- License & Community
Web interfaces frequently force developers to pick between two extremes:
- Low-level Imperative DOM APIs: Manual
createElement, verbose event listeners, and brittle DOM state synchronization. - Heavyweight Toolchains: Mandatory JSX compilers, virtual DOM diffing overhead, and specialized templating syntax.
Jetz offers a third way: declarative, composable JavaScript with reactive state management, without leaving standard JavaScript.
Traditional DOM Code:
create element → configure element → query element → append element → manually mutate element
Jetz Paradigm:
describe UI → compose components → declare reactive state → DOM updates automatically
- UI = JavaScript Composition: Build DOM trees with simple, readable function calls (
div,button,p). - State = Reactive: State values notify bound elements directly; no virtual-DOM diffing passes required.
- Components = Composable: Package UI into pure functions or reusable classes with full lifecycle hooks.
- DOM = Direct and Lightweight: Clean abstraction over native elements that preserves direct element access when needed.
The entire Jetz mental model comes down to four basic steps:
import { Jetz, stateOf } from "@daevsoft/jetz";
import { button, div, h1 } from "@daevsoft/jetz/ui";
// 1. Create reactive state
const count = stateOf(0);
// 2. Compose your UI tree
const App = () => div(
h1("Interactive Counter"),
// 3. React to state: pass state directly or update it on event
button({ onclick() {
count.value++; // automatically triggers fine-grained DOM update
} }, "Clicked ", count, " times")
);
// 4. Mount to your HTML document
Jetz.mount(App, "#app");No build step required to parse custom syntax. That is valid, executable JavaScript out of the box.
| Feature | Built-in API | What It Solves |
|---|---|---|
| Declarative UI | div, span, button, inputText, ... |
Compose HTML elements cleanly with nested function calls. |
| Components | Functions or extends Component |
Reusable UI units with input parameters and private state. |
| Reactive State | stateOf(value) |
Fine-grained single-value state with subscribers and watchers. |
| Remembered State | rememberOf(key, value) |
State synchronized with localStorage across page reloads. |
| Derived State | computed(fn) |
Auto-tracked computed values with zero manual dependency arrays. |
| Reactive Classes | css(fn) |
Class attributes that re-evaluate whenever the state they read changes. |
| Side-Effects | effect(fn) |
Auto-tracking effects with instant execution and disposal cleanup. |
| Reactive Lists | listOf(), sequenceOf() |
Observable arrays with chainable methods (push, remove, sort). |
| Keyed Reconciliation | loop(list, keyFn, renderFn) |
O(1) DOM element recycling and minimal mutations on array changes. |
| Conditional UI | _if, _elseif, _else, ifElse |
Declarative, reactive conditional rendering without wrapper divs. |
| Component Lifecycle | onCreate, onMount, onUpdate, onDestroy |
Deterministic setup and teardown for function and class components. |
| Two-Way Binding | { bind: state } |
Instant two-way synchronization between input elements and state. |
| Dynamic Routing | Router, route, link, asLink |
Client-side SPA routing with browser history and parameters. |
| Route Guards | Middleware, middleware |
Async/sync navigation guards with reason-based denials. |
| Session State | JetzSession, sessionOf |
Reactive key-value store automatically backed by sessionStorage. |
| Dispatcher | Dispatcher |
Flux-like action dispatching for clean architecture. |
| DOM Utilities | find, findAll, .attr(), .addClass(), ... |
Fluent helper methods on every element before and after mounting. |
Adopting Jetz is a smooth shift from manual DOM plumbing to declarative composition:
HTML markup ───► JavaScript composition functions
Manual createElement / append ───► Nested function hierarchies
Manual DOM innerText updates ───► Reactive stateOf and computed values
Spaghetti event listeners ───► Inline declarative handler objects
Full list re-renders ───► Keyed loop reconciliation
Complex external build configs ───► Standard ES Modules and standard JS
// Verbose, error-prone, manual synchronization
const container = document.createElement("div");
container.className = "card";
const counterText = document.createElement("p");
let count = 0;
counterText.textContent = `Count: ${count}`;
const btn = document.createElement("button");
btn.textContent = "Increment";
btn.addEventListener("click", () => {
count++;
counterText.textContent = `Count: ${count}`; // Manual sync required
});
container.appendChild(counterText);
container.appendChild(btn);
document.body.appendChild(container);// Declarative, reactive, clean composition
import { Jetz, stateOf } from "jetz";
import { div, p, button, css } from "jetz/ui";
const count = stateOf(0);
const Card = div(css`card`,
p("Count: ", count),
button("Increment", {
onclick: () => count.value++
})
);
Jetz.mount(Card, document.body);The difference: In Jetz, the relationship between state and UI is declared once. When state changes, only the exact bound text node or attribute updates.
Many modern frameworks rely on JSX or custom template compilers (.vue, .svelte, .html). Jetz intentionally uses standard JavaScript functions:
- Zero Build Overhead for Syntax: You can run Jetz in modern browsers, prototyping tools, or standard ESM setups without requiring Babel, SWC, or TypeScript JSX transforms just to render an element.
- Full Power of JavaScript: Functions are just functions. Variables, loops, closures, conditionals, arrays, and standard language features work directly without templateDSL constraints.
- Transparent DOM Mapping:
div(...)creates aJetzElementthat wraps and produces real DOM elements directly. There are no hidden virtual DOM reconciliation layers getting between you and the browser. - Composable by Nature: Passing elements, components, or UI fragments as parameters, returning them from helpers, or composing them dynamically is as natural as writing regular JavaScript functions.
- Interactive Web Apps & SPAs: Full routing, state, session, and lifecycle built into a lightweight package.
- Dashboards & Internal Tools: Rapid prototyping and clean UI creation without heavy build tooling.
- Component-Driven Frontends: Teams and developers who prefer the clarity of functional composition over JSX.
- Performance-Sensitive Micro-UIs: Situations where virtual-DOM runtime overhead and large bundle sizes are unwanted.
- Projects where the engineering team is strictly mandated to use JSX/TSX syntax.
- Content-heavy static sites with zero interactivity (where plain static HTML/SSG is sufficient).
Install Jetz and the Rspack bundler tools:
# Using pnpm
pnpm add @daevsoft/jetz
pnpm add -D @rspack/core @rspack/cli
# Using npm
npm install @daevsoft/jetz
npm install -D @rspack/core @rspack/cliHere is a minimal, complete single-page application setup with Rspack and Jetz routing:
my-jetz-app/
├── index.html
├── index.js
├── rspack.config.js
├── package.json
└── src/
├── app.js
└── home.js
import { rspack } from "@rspack/core";
export default {
entry: "./index.js",
plugins: [
new rspack.HtmlRspackPlugin({
template: "./index.html",
}),
],
devServer: {
hot: false,
},
};<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jetz App</title>
</head>
<body>
<div id="app"></div>
</body>
</html>import { Jetz } from "@daevsoft/jetz";
import { main } from "@daevsoft/jetz/ui";
export const App = () => {
return main(
Jetz.$route.browser()
);
};import { css, div } from "@daevsoft/jetz/ui";
export const Home = () => {
return div(css`text-gray-500`, "Hello World");
};import { Jetz } from "@daevsoft/jetz";
import { route, Router } from "@daevsoft/jetz/router";
import { Home } from "./src/home.js";
import { App } from "./src/app.js";
const router = new Router([
route('/', Home)
]);
Jetz.use(router);
Jetz.mount(App, '#app');Add this script to your package.json:
{
"type": "module",
"scripts": {
"dev": "rspack serve",
"build": "rspack build"
}
}Then start the server:
npm run dev
# or
npx rspack serveFollow this structured guide to master Jetz step by step:
- Elements & UI Composition — Learn how HTML tags map to functions
- Components — Functional and class-based components
- Reactive State — State management with
stateOfandrememberOf - Computed State — Auto-tracked derived values with
computed - Side-Effects — Reactive watchers with
effect - Reactive Collections — Arrays with
listOfandsequenceOf - Keyed List Reconciliation — Fast list rendering with
loop - Conditional Rendering — Declarative branch switching
- Component Lifecycle —
onCreate,onMount,onUpdate,onDestroy - Two-Way Binding — Synchronizing form inputs
- Reactive Listeners — Dynamic styling & DOM reactions with
listen - DOM Utilities — Fluent element manipulation helpers
- Router & Middleware — Multi-page SPA navigation
- Session Storage — Tab-persistent state
Every HTML5 element is exported as a composable JavaScript function from jetz/ui:
import { div, h1, p, span, button, a, img, css } from "jetz/ui";
const Banner = div(
h1("Fast, Declarative UI"),
p("Composable JavaScript functions represent elements:"),
span("No templates. No JSX."),
button("Get Started", {
onclick: () => alert("Welcome!")
})
);Element functions accept arguments in any natural order:
- Strings & Numbers: Rendered as child text nodes.
- Child Elements: Appended directly into the parent.
- Objects: Configured as attributes, properties, or event handlers. Keys written
data_*/aria_*render hyphenated, so{ data_counter: n }writesdata-counter. - CSS Helpers: Tagged template
css\class-name`, a reactive functioncss(() => …)`, or style objects. - Reactive States: Automatically bind their text content.
div(
css`card active`, // classes
{ id: "hero", role: "banner" }, // attributes
h2("Title"), // child element
"Text content" // text node
)Multiple css() calls on one element are merged, so reactive and static classes can be declared side by side. See Reactive Classes.
You can define components in three ways: as variables, functions, or classes extending Component.
Function components are simple JavaScript functions that return an element tree:
import { div, h3, p, css } from "jetz/ui";
export function UserCard(name, role) {
return div(css`user-card`,
h3(name),
p(role)
);
}
// Usage in parent:
const Page = div(
UserCard("Alice", "Frontend Engineer"),
UserCard("Bob", "Product Designer")
);For complex stateful components or object-oriented architectures, extend Component:
import { Component, stateOf } from "jetz";
import { div, button } from "jetz/ui";
export class CounterComponent extends Component {
count = stateOf(0);
increment() {
this.count.value++;
}
render() {
return div(
button("Clicked: ", this.count, {
onclick: () => this.increment() // use arrow function to preserve `this`
})
);
}
}
// Instantiate with `new` or `.new()`:
const App = div(
new CounterComponent(),
CounterComponent.new()
);State in Jetz is created using stateOf(initialValue).
import { stateOf } from "jetz";
import { div, button } from "jetz/ui";
const counter = stateOf(0);
// Reading & writing state
console.log(counter.value); // 0
counter.value = 10; // updates value & triggers UI re-renders
counter.setState(20); // same as counter.value = 20
// Manual subscriptions (if needed)
const unsubscribe = counter.subscribe((newValue, oldValue) => {
console.log(`Changed from ${oldValue} to ${newValue}`);
});Assign to .value to update a reactive value; use setState() when you prefer an explicit setter. Both forms notify subscribers and update bound UI:
const count = stateOf(0);
count.value += 1;
count.setState(10);
const profile = stateOf({ name: "Ada" });
profile.name.value = "Grace"; // object properties are reactive states tooUse stateOf for temporary UI or application state. Use a ListState for collections that need reactive add, remove, or replace operations.
Any value can be held, including falsy ones. null, false, 0, "", and undefined are all valid initial values, so a state can start out "empty" and be filled in later:
const filter = stateOf(""); // starts blank
const ready = stateOf(null); // no value yet
const chosen = stateOf(false);rememberOf(key, initialValue) works like stateOf, but synchronizes supported updates to localStorage and restores them on refresh. The key should remain stable between visits; remembered values are scoped to the current page path.
import { rememberOf } from "jetz";
const theme = rememberOf("theme", "light");
theme.value = "dark"; // automatically saved to localStorage
// Arrays can also be remembered:
const recentSearches = rememberOf("searches", []);
recentSearches.push("JavaScript"); // persistedFor remembered arrays, push(), set(), and clear() save automatically. Other list mutations still update the reactive UI, but do not currently write to storage. To persist a removal or other transformed result, replace the list with set():
recentSearches.set(recentSearches.values.filter(item => item !== "JavaScript"));Store JSON-serializable data in remembered arrays. If list items need reactive fields, save plain data and recreate the reactive item states when loading.
computed(fn) creates derived state that automatically tracks its dependencies.
When any state accessed inside the computation function changes, the computed value re-evaluates automatically and updates all bound DOM elements:
import { stateOf, computed } from "jetz";
import { div, span, inputText } from "jetz/ui";
const firstName = stateOf("Ada");
const lastName = stateOf("Lovelace");
// Automatically tracks `firstName` and `lastName`
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
const UserProfile = div(
span("Full Name: ", fullName), // Updates whenever firstName or lastName changes
inputText({ bind: firstName, placeholder: "First name" }),
inputText({ bind: lastName, placeholder: "Last name" })
);Computed states are fully reactive states and can depend on other computed states:
const price = stateOf(100);
const qty = stateOf(2);
const subtotal = computed(() => price.value * qty.value);
const tax = computed(() => subtotal.value * 0.1);
const grandTotal = computed(() => subtotal.value + tax.value);css accepts either a tagged template or a plain function, and both can be reactive. Pick whichever reads better:
| Form | Use it when |
|---|---|
css`base ${() => …}` |
Only part of the class depends on state. |
css(() => …) |
The whole class is derived from state. |
In both cases the callback is evaluated as a computed value, and the element's class attribute is rewritten whenever the state it reads changes.
Use a callback interpolation when part of a class depends on state:
import { listOf, loop, stateOf } from "jetz";
import { css, li, ul } from "jetz/ui";
const tasks = listOf(
stateOf({ id: 201, done: true, title: "Sketch the onboarding flow" }),
stateOf({ id: 202, done: false, title: "Review the component API" })
);
const TaskList = ul(loop(tasks, task => li(
css`task-number ${() => task.done.value ? "completed" : ""}`,
task.title
)));The callback must read task.done.value; an expression like ${task.done ? "completed" : ""} is evaluated before css receives it and cannot track future changes. Static classes remain ordinary tagged templates, for example csstask-number completed``.
When the whole class depends on state, pass a function instead of a tagged template:
import { stateOf } from "jetz";
import { css, div } from "jetz/ui";
const count = stateOf(0);
const Counter = div(
css(() => count.value % 2 === 0 ? "text-red-200" : "text-green-200")
);Return null to drop the class entirely - handy for conditional styling that should leave no residue when inactive:
const ready = stateOf(false);
const Badge = div(css(() => ready.value ? "is-ready" : null));Several css() calls on the same element are merged, and every reactive part keeps updating. Static classes are preserved across recompositions:
const active = stateOf(true);
const Panel = div(
css("panel base"), // always present
css(() => active.value ? "is-active" : "is-idle") // swaps on change
);The same two forms work as a plain attribute, written either as class or as its className alias:
const count = stateOf(0);
const Heading = div({ class: () => (count.value ? "wow" : "now") }, "A counter");
const Same = div({ className: () => (count.value ? "wow" : "now") }, "A counter");className is the DOM property name, so it is accepted and mapped onto class before attributes are merged - it combines with any css() on the same element instead of overwriting it.
Only class and className reach the class attribute. Any other key reaches the DOM under the name you wrote - apart from the data_/aria_ underscore rule shown under Reactive Attributes - and HTML attribute names are case-insensitive, so div({ cssClass: () => … }) renders <div cssclass="now">. The callback does run, and keeps re-running - it updates an attribute that nothing styles, which is why the element looks inert. Jetz maps this one well-known alias and never guesses at the rest, so viewBox and preserveAspectRatio keep their casing and a mistyped key stays visible in the markup rather than being silently rerouted.
Two rules cover every form above:
- Read
.valueinside the callback. A callback tracks the states it reads, so() => count.value % 2updates while${count.value % 2 ? "a" : "b"}is evaluated beforecssever sees it and freezes at its first value. - Return
nullorfalseto contribute nothing. Those values are filtered out instead of being stringified, so no strayclass="false"is left behind.
Attribute helpers keep State values instead of converting them to strings early. You can also pass a callback to an attribute or style property; its .value reads are tracked and the DOM updates when they change:
import { stateOf } from "jetz";
import { a, aria_, data_, button, href, style } from "jetz/ui";
const destination = stateOf("/tasks");
const saving = stateOf(false);
const color = stateOf("crimson");
const Link = a(href(destination), "Tasks");
const SaveButton = button({ disabled: saving }, "Save");
const Status = a(
aria_({ busy: saving }),
data_({ destination }),
{ title: () => `Open ${destination.value}` },
"Status"
);
const Swatch = a(style({ color: () => color.value }), "Preview");HTML boolean attributes such as disabled are added for true and removed for false. aria-* and data-* values remain strings, so a false state becomes "false" rather than removing the attribute.
Underscores work for these two prefixes, which spares an object literal its quotes:
import { stateOf } from "jetz";
import { css, data_, div, p } from "jetz/ui";
const counter = stateOf(0);
const Row = p(css`counter-${counter}`, { data_counter: counter }, "Counter:", counter);
// renders data-counter="0" - [data-counter] and element.dataset.counter both find it
const Same = p(data_({ counter }), "Counter:", counter); // helper form, same attribute
const Note = div({ aria_labelledby: "total", data_row_index: 2 }); // aria-labelledby, data-row-indexEvery _ after a data or aria prefix becomes a -, so data_row_index reads back as dataset.rowIndex. No other key is rewritten - source_map and my_data stay exactly as written - and a key that merely starts with those letters (database_id) is left alone.
Two things to keep in mind:
- Write the tail lowercase. HTML lowercases attribute names, so
data_rowIndexlands asdata-rowindexanddataset.rowIndexmisses it;data_row_indexis the spelling that round-trips. - Pick one spelling per element.
{ data_counter: a }anddata_({ counter: b })name the same attribute, and a duplicated non-class attribute is merged into a list that onlyclassknows how to use - both values are dropped.
effect(fn) runs an imperative side-effect function that automatically discovers its dependencies by intercepting .value reads.
It re-executes whenever any accessed state changes, and returns a dispose function:
import { stateOf, effect } from "jetz";
const counter = stateOf(0);
// Executes immediately, then re-runs on every counter change:
const dispose = effect(() => {
document.title = `Count: ${counter.value}`;
});
// Dynamic conditional tracking:
const loggingEnabled = stateOf(true);
const status = stateOf("idle");
effect(() => {
if (loggingEnabled.value) {
console.log("Current status:", status.value); // tracks status only when loggingEnabled is true
}
});
// Clean up when no longer needed:
dispose();For dynamic arrays, Jetz provides reactive collections via listOf() and sequenceOf(). Use their mutation methods when the collection itself changes; they update rendered lists created with loop():
import { listOf, loop } from "jetz";
import { ul, li } from "jetz/ui";
const todos = listOf("Learn Jetz", "Build an App");
const TodoList = ul(loop(todos, todo => li(todo)));
todos.push("Test the app");
todos.remove("Build an App");
todos.set(["Ship the feature"]);Use push() to append, insertAt() to insert at a known position, remove() / removeAt() to delete, and set() to replace the full collection (for example, after filtering or loading new data). Use sequenceOf() when creating a list whose duplicate primitive values must remain distinct, or call asUnique() on an existing list.
Call asRemember(key) when an existing ListState should be restored from and saved to localStorage. Prefer an explicit, stable key so the list does not depend on creation order. Use rememberOf(key, initialValue) when you want to create a remembered value directly; use asRemember() when you already have a list to mark as remembered:
const selectedTags = listOf("news", "news").asUnique();
const recentSearches = listOf().asRemember("recent-searches");
recentSearches.push("Jetz"); // saved to localStorage| Method / Property | Description |
|---|---|
list.push(...items) |
Append one or more items and update rendered lists |
list.set(newArray) |
Replace all items; chainable |
list.map(fn) |
Return a new array of mapped values without changing the list |
list.insertAt(index, ...items) |
Insert items at an index |
list.remove(item) |
Remove the first matching item |
list.removeAt(index) |
Remove the item at an index |
list.sort((a, b) => ...) |
Sort items in place; chainable |
list.filter(predicate) |
Return a plain array without changing the list |
list.clear() |
Empty the collection |
list.asUnique() |
Make duplicate string/number values distinct entries; chainable |
list.asRemember(key?) |
Restore and persist the list using an optional stable key; chainable |
list.size |
Returns item count |
list.values |
Direct reference to underlying array |
list.first() / list.last() |
Convenience accessors for boundary items |
ListState.map() follows the standard array behavior and returns a new array. Use transform() when you want to replace list items and update rendered views. Avoid changing list.values or nested item properties in place when you need a rendered update; use a ListState method or set() with the updated array. For a remembered list, use set() after mutations other than push(), set(), or clear() to save the result. asRemember() stores JSON-serialized values, so prefer plain serializable records over reactive State instances.
When rendering large collections, full list re-rendering can be costly. Jetz provides keyed reconciliation through the 3-argument form of loop():
import { listOf, loop } from "jetz";
import { ul, li, button, div } from "jetz/ui";
const users = listOf(
{ id: 101, name: "Alice" },
{ id: 102, name: "Bob" }
);
const UserList = ul(
loop(
users,
user => user.id, // Key selector: unique identifier
user => li(user.name) // Render function
)
);Without Keys (classic loop):
1000 items ──(1 item modified)──► Re-render all 1000 DOM elements
With Keys:
1000 items ──(1 item modified)──► Re-render ONLY the 1 modified DOM element
The classic 2-argument form loop(list, renderFn) remains available for backward compatibility.
Jetz supports declarative conditional rendering without creating unnecessary wrapper elements.
import { stateOf, _if, _elseif, _else } from "jetz";
import { div, button } from "jetz/ui";
const tab = stateOf("home");
const Content = div(
div(_if(() => tab.value === "home"), "Welcome to the Homepage"),
div(_elseif(() => tab.value === "profile"), "Your Profile Details"),
div(_else, "Page Not Found"),
button("Switch", {
onclick: () => tab.setState(tab.value === "home" ? "profile" : "home")
})
);ifElse(conditionFn, trueBranch, falseBranch) evaluates inline and swaps content seamlessly in place:
import { stateOf, ifElse } from "jetz";
import { div, span, button } from "jetz/ui";
const isLoggedIn = stateOf(false);
const Nav = div(
ifElse(
() => isLoggedIn.value,
() => span("Welcome back!"),
() => button("Log In", { onclick: () => isLoggedIn.setState(true) })
)
);Every Jetz component — whether function-based or class-based — supports four lifecycle stages:
onCreate ──► onMount ──► onUpdate ──► onDestroy
(before DOM) (in DOM) (state delta) (removed)
| Lifecycle Hook | Timing & Purpose |
|---|---|
onCreate |
Executes once before rendering occurs. Ideal for initializing local variables and preparing state. |
onMount |
Executes once immediately after the element is attached to the document. Safe to touch DOM, start timers, or fetch data. |
onUpdate |
Executes whenever a bound state changes while the component is active in the DOM. |
onDestroy |
Executes when the element is removed from DOM (e.g. route change, conditional removal, list delete, unmount). Ideal for clearing timers and subscriptions. |
import { onCreate, onMount, onUpdate, onDestroy, stateOf } from "jetz";
import { div } from "jetz/ui";
export function LiveClock() {
const time = stateOf(new Date().toLocaleTimeString());
let intervalId;
onCreate(() => console.log("Clock initializing..."));
onMount(() => {
intervalId = setInterval(() => {
time.setState(new Date().toLocaleTimeString());
}, 1000);
});
onUpdate(() => console.log("Clock updated to:", time.value));
onDestroy(() => {
clearInterval(intervalId);
console.log("Clock cleaned up.");
});
return div("Current Time: ", time);
}import { Component, stateOf } from "jetz";
import { div } from "jetz/ui";
export class LiveClockClass extends Component {
time = stateOf("");
intervalId = null;
onMount() {
this.intervalId = setInterval(() => {
this.time.value = new Date().toLocaleTimeString();
}, 1000);
}
onDestroy() {
clearInterval(this.intervalId);
}
render() {
return div("Time: ", this.time);
}
}Bind any stateOf instance directly to an input element using the bind property:
import { stateOf } from "jetz";
import { div, inputText, p } from "jetz/ui";
const query = stateOf("");
const SearchBox = div(
inputText({
bind: query, // Two-way binding: updates query.value on user input
placeholder: "Search documentation..."
}),
p("Searching for: ", query)
);Whenever the user types, query.value updates immediately. Conversely, setting query.value = "something" updates the input field's display value automatically.
listen(callback) registers an inline reactive effect tied directly to an element. It renders no DOM element of its own, executing once on mount and subsequently on every state change:
import { stateOf, listen } from "jetz";
import { div, button } from "jetz/ui";
const count = stateOf(0);
const Box = div(
"Current count: ", count,
// Automatically updates the parent div's CSS class as state changes:
listen(parent => {
parent.replaceClass(/count-\d+/, `count-${count.value}`);
}),
button("+1", { onclick: () => count.value++ })
);Every JetzElement wraps an HTMLElement and provides a fluent chainable API that works both before and after mounting:
import { div, find, findAll } from "jetz/ui";
const box = div("Hello World", { "data-role": "card" });
// Attributes & Data
box.attr("data-role"); // Read attribute
box.addAttr("title", "Greetings"); // Set attribute
box.removeAttr("title"); // Remove attribute
box.data("role"); // Read data-* property
// Styling & Classes
box.setStyle({ color: "blue" }); // Apply inline styles
box.getStyle("color"); // Read style property
box.addClass("active"); // Add class
box.removeClass("active"); // Remove class
box.toggleClass("active"); // Toggle class
box.replaceClass("active", "idle");// Replace class
// DOM Tree & State
box.text("Updated Text"); // Replace text content
box.disable(); box.enable(); // Toggle disabled state
box.focus(); box.blur(); // Focus controls
box.empty(); // Remove all children
box.remove(); // Remove element from DOM
// Global Finders
const header = find("#main-header"); // Returns HTMLElement
const items = findAll(".list-item"); // Returns Array<HTMLElement>Jetz is not just a DOM builder — it scales cleanly from a simple inline element to a full Single Page Application (SPA):
1. Element
└─ div("Hello World")
2. Component
└─ function Header(title) { return header(h1(title)); }
3. Reactive State & Computed
└─ const count = stateOf(0); const double = computed(() => count.value * 2);
4. Reactive Collections
└─ const items = listOf(); items.push(...)
5. Lifecycle & Effects
└─ onMount(() => ...); effect(() => ...);
6. Routing & Middleware
└─ new Router(route('/', Home), middleware(AuthGuard, route('/admin', Admin)))
7. Session Storage
└─ const session = new JetzSession(); Jetz.use(session);
8. Production Application
└─ Jetz.mount(App, document.body);
Jetz includes an integrated client-side SPA router:
import { Jetz } from "jetz";
import { Router, route, link, asLink, asBackLink, redirect } from "jetz/router";
import { div, nav, main } from "jetz/ui";
// Define view components
const HomeView = () => div("Welcome Home");
const AboutView = () => div("About Us");
// Configure router
const appRouter = new Router(
route("/", HomeView),
route("/about", AboutView)
);
// Install router into Jetz
Jetz.use(appRouter);
// Compose App Shell
function AppShell() {
return main(
nav(
link("/", div("Home")),
link("/about", div("About"))
),
// Mount router viewport:
Jetz.$route.browser()
);
}
Jetz.mount(AppShell(), document.body);Attach a head callback to a route to update the document title and metadata whenever that route is activated. Return one head element or an array of elements; Jetz removes the previously managed route elements while preserving unrelated tags already in <head>.
import { Router, route } from "jetz/router";
import { title, meta } from "jetz/ui";
const appRouter = new Router(
route("/", HomeView),
route("/about", {
component: AboutView,
head: () => [
title("About Jetz"),
meta({
name: "description",
content: "Learn about the Jetz framework."
})
]
})
);This keeps metadata current during client-side navigation. For search and social crawlers that need metadata in the initial HTML response, use server-side rendering or prerendering as well; client-side updates alone cannot add tags to the response already delivered by the server.
Use :name for a dynamic path segment. Captured values are URL-decoded and passed to the route component, middleware, and head callback. Exact static paths take precedence over dynamic matches.
const router = new Router(
route("/order/:orderId/message", {
component: ({ orderId }) => div(`Messages for order ${orderId}`),
head: ({ orderId }) => title(`Messages for order ${orderId}`)
})
);
router.to("/order/A%2012/message"); // orderId is "A 12"Use group() to share a path prefix and middleware across nested routes. route() continues to define individual endpoints:
import { Jetz } from "jetz";
import { Router, group, route } from "jetz/router";
const routes = [
route("/", Home),
group("/admin", {
middlewares: AuthGuard,
routes: [
route("/", Dashboard),
group("/users", {
middlewares: [AdminGuard, RoleGuard],
routes: [
route("/", UserList),
route("/:id", UserDetail)
]
})
]
})
];
const router = new Router(routes);
Jetz.use(router);The nested paths resolve to /admin, /admin/users, and /admin/users/:id. Parent middleware runs before child middleware, so the user detail route runs AuthGuard, AdminGuard, then RoleGuard.
asLink("/path", params): Event modifier to navigate to a route on click.asBackLink: Triggershistory.back()on click.redirect("https://example.com"): Programmatic full-page navigation.
Protect routes with custom middlewares extending Middleware:
import { Router, route } from "jetz/router";
import { Middleware, middleware } from "jetz/middleware";
class AuthGuard extends Middleware {
next(params, _continue) {
const isAuthenticated = Boolean(localStorage.getItem("token"));
if (!isAuthenticated) {
return this.deny("User is not authenticated");
}
return true; // Allow navigation
}
}
class AdminGuard extends Middleware {
next(params, _continue) {
const user = JSON.parse(sessionStorage.getItem("user") || "null");
if (user?.role !== "admin") {
return this.deny("Administrator access is required");
}
return true;
}
}
const router = new Router(
route("/", HomeView),
// Every guard must return true for navigation to proceed.
middleware([AuthGuard, AdminGuard],
route("/admin", AdminView)
),
// Multiple routes can share one middleware too.
middleware(AuthGuard,
route("/dashboard", DashboardView),
route("/settings", SettingsView)
)
);Middleware arrays run in order. Navigation stops at the first guard that denies it, so /admin requires both authentication and the administrator role.
Manage persistent browser session state backed by sessionStorage:
import { Jetz } from "jetz";
import { JetzSession, sessionOf } from "jetz/session";
// 1. Create a managed session
const session = new JetzSession({ user: null, theme: "dark" });
Jetz.use(session); // Exposes Jetz.$session
// Direct property mutations persist automatically:
session.theme = "light";
// Explicit key-value methods:
session.set("user", { id: 1, name: "Alice" });
session.get("user"); // { id: 1, name: "Alice" }
session.has("user"); // true
session.destroy(); // Clear storage and reset defaults
// 2. Standalone reactive session proxy
const localSession = sessionOf({ activeFilter: "all" });
localSession.activeFilter = "completed"; // persistedFor applications requiring an explicit unidirectional data flow, Jetz includes Dispatcher:
import { stateOf, Dispatcher } from "jetz";
import { main, nav, ul, li } from "jetz/ui";
const currentPage = stateOf("home");
const dispatcher = new Dispatcher(action => {
switch (action) {
case "NAV_HOME":
currentPage.setState("home");
break;
case "NAV_ABOUT":
currentPage.setState("about");
break;
}
});
const App = main(
nav(
ul(
li("Home", { onclick: () => dispatcher.dispatch("NAV_HOME") }),
li("About", { onclick: () => dispatcher.dispatch("NAV_ABOUT") })
)
),
currentPage
);import { addScript, html } from "jetz";
import { div } from "jetz/ui";
// Dynamically load an external script with callback
addScript("https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js", {
async: true,
onload: () => console.log("Confetti library loaded!")
});
// Render raw HTML safely wrapped in a Raw element
const RawBox = div(html("<strong>Formatted HTML snippet</strong>"));Jetz provides convenient lightweight utility extensions:
import { range, flatMap, createList } from "jetz";
range(1, 4); // [1, 2, 3, 4]
flatMap([1, [2, [3]], 4]); // [1, 2, 3, 4]
createList(3, i => `Item ${i}`); // ['Item 0', 'Item 1', 'Item 2']
// Extended prototypes:
[10, 20, 30].last(); // 30
[10, 20, 30].take(2); // [10, 20]
(3).range(6); // [3, 4, 5, 6]
document.querySelectorAll("li").last(); // Last matched DOM nodeHere is how real Jetz applications compose state, collections, UI elements, and styling together.
(Adapted from the built-in Calculator Demo):
import { Jetz, stateOf, listOf, loop } from "jetz";
import { div, h1, button, span, css } from "jetz/ui";
// 1. Reactive State & History Collection
const display = stateOf("0");
const history = listOf();
let currentInput = "0";
function inputDigit(digit) {
currentInput = currentInput === "0" ? digit : currentInput + digit;
display.value = currentInput;
}
function clearAll() {
currentInput = "0";
display.value = "0";
}
function evaluateResult() {
const result = String(eval(currentInput) || 0); // Simplified for illustration
history.push({ expr: currentInput, result });
currentInput = result;
display.value = result;
}
// 2. Composable UI Tree
export function CalculatorApp() {
return div(
css`max-width: 380px; margin: 40px auto; padding: 20px; font-family: sans-serif;`,
h1("Jetz Calculator"),
// Screen display: bound to `display` state
div(
css`background: #1e293b; color: #fff; font-size: 32px; padding: 16px; text-align: right; border-radius: 8px;`,
display
),
// Keypad Grid
div(
css`display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 12px;`,
button("7", { onclick: () => inputDigit("7") }),
button("8", { onclick: () => inputDigit("8") }),
button("9", { onclick: () => inputDigit("9") }),
button("C", { onclick: clearAll }),
button("4", { onclick: () => inputDigit("4") }),
button("5", { onclick: () => inputDigit("5") }),
button("6", { onclick: () => inputDigit("6") }),
button("=", { onclick: evaluateResult })
),
// History Panel: reactive loop
div(
css`margin-top: 20px; border-top: 1px solid #ccc; padding-top: 12px;`,
span("History:"),
loop(history, item => item.expr, item => (
div(span(`${item.expr} = `), span(item.result))
))
)
);
}
Jetz.mount(CalculatorApp(), document.body);┌─────────────────────────────────────────────────────────────┐
│ Jetz Application │
└──────────────────────────────┬──────────────────────────────┘
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Components │ │ Reactive │ │ App Layer │
│ & Elements │ │ Engine │ │ │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ • div, p, ...│ │ • stateOf │ │ • Router │
│ • Component │ │ • computed │ │ • Middleware │
│ • Lifecycle │ │ • effect │ │ • Session │
│ • Binding │ │ • listOf │ │ • Dispatcher │
│ • JetzElement│ │ • loop │ │ • Plugins │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└──────────────────────┼──────────────────────┘
▼
┌─────────────────────────┐
│ Direct DOM Render │
│ (No Virtual DOM Diffing)│
└─────────────────────────┘
stateOf(initialValue): Create reactive single value.rememberOf(key, initialValue): Reactive value persisted inlocalStorage.computed(fn): Auto-tracked derived state.effect(fn): Auto-tracked imperative effect (returnsdispose).listen(callback): Reactive inline listener attached to element.css(...): Class attribute; the function formcss(() => …)re-evaluates on state change.
listOf(...items): Reactive array with helper mutation methods.sequenceOf(...items): Reactive unique sequence array.loop(list, keyFn, renderFn): Key-reconciled list rendering.
_if(conditionFn): Conditional branch render._elseif(conditionFn): Alternate conditional branch._else: Default branch.ifElse(cond, trueFn, falseFn): Inline two-way conditional.
Component: Base class for OOP-style components.onCreate(fn): Runs before initial render.onMount(fn): Runs after DOM attachment.onUpdate(fn): Runs on subsequent state changes.onDestroy(fn): Runs on DOM removal.
Router,route: Dynamic SPA client routing.link,asLink,asBackLink: Route navigation helpers.Middleware,middleware: Navigation guards.JetzSession,sessionOf: Reactive session store.
Jetz.mount(App, container): Mount application to DOM.Jetz.unmount(container): Cleanly teardown mounted elements.Jetz.style(cssString): Inject dynamic<style>rules.Jetz.use(plugin): Install plugin (e.g. Router, Session).find(selector),findAll(selector): DOM query helpers.
Jetz is backed by three test suites covering all features, reactivity, and browser environments:
# 1. Fast unit suite (Vitest + jsdom)
pnpm test:unit
# 2. Browser smoke suite (Puppeteer / real Chrome & Edge)
pnpm test:smoke
# 3. Built pages assertion suite (Rspack bundle verification)
pnpm test:pages
# Run all suites together:
pnpm testThis repository is organized as a workspace with fast builds powered by Rspack:
# Install dependencies
pnpm install
# Watch mode for active development
pnpm watch
# Start local development server
pnpm start
# Create optimized production build
pnpm buildThe shippable npm package is located under packages/jetz:
cd packages/jetz
# Validate exports and generate TypeScript stubs
npm run check-exports
# Publish package
npm publish --access publicJetz is open-source software licensed under the ISC License.
- Repository: https://github.com/devarofi/jetz
- Author: @daevsoft
- Issues & Feedback: GitHub Issues
If you find Jetz helpful, please give the repository a ⭐ on GitHub!