Signal-based reactivity that runs natively with ESM/import maps. No virtual DOM. No compiler. Use DOM-first JS libraries directly (Chart.js, Leaflet, AG Grid) and pick your path: web with Nix-UI or mobile with Nix-Ionic. Nix Query works in both.
Compose interfaces with Nix-UI, and plug Nix Query when you need server-state, retries, and cache.
Launch web apps -> Mobile PathIonic routing + Nix.js reactivity for one codebase targeting web, Android, and iOS, with optional Nix Query data layer.
Launch mobile apps ->We include the router, forms, and state management in a bundle smaller than other frameworks' core runtimes.
* Gzipped sizes including core + standard internal features.
Nix.js is not reinventing UI from scratch. It combines proven ideas from frameworks developers already trust: tagged templates, fine-grained signals, provide/inject, function components, and auto-tracking.
Real integration pattern: use refs, lifecycle hooks, and cleanup exactly as you would in production code.
Teams often reject frameworks when integration with existing JS libraries is painful. Nix.js keeps the native DOM model, so you can plug in charting, maps, grids, editors, and media players directly.
import { NixComponent, html, signal, effect, ref } from "@deijose/nix-js"; import { Chart } from "chart.js/auto"; class SalesChart extends NixComponent { private canvasRef = ref<HTMLCanvasElement>(); private points = signal([12, 19, 7]); private chart = null; render() { return html`<canvas ref=${this.canvasRef}></canvas>`; } onMount() { const ctx = this.canvasRef.el?.getContext("2d"); if (!ctx) return; this.chart = new Chart(ctx, { type: "line", data: { datasets: [{ data: this.points.value }] } }); effect(() => { this.chart.data.datasets[0].data = this.points.value; this.chart.update(); }); return () => this.chart?.destroy(); } }
No compiler, no config files, no boilerplate. Just install, write, and go.
One package, zero runtime dependencies. Works with Vite, Webpack, or directly via ESM CDN.
A plain function returning html``
is all you need. No class, no decorator, no JSX transform.
Call mount()
once. Every signal update after this happens automatically — no re-render calls, no manual DOM updates.
tsconfig.json,
no vite.config.ts,
no babel.config.js
required — run it straight from the browser with an import map.
index.htmlA complete UI framework that fits in a single import. No virtual DOM overhead, no compiler step, no configuration files.
Signals update only the exact DOM nodes that depend on changed data. No diffing, no reconciliation, no wasted renders.
Templates are standard JavaScript tagged template literals. No JSX transform, no SFC compiler, no build-time magic needed.
Router, forms, stores, dependency injection, portals, error boundaries, transitions — all built-in. One import, zero config.
Every API is fully typed from the ground up. Typed injection keys, typed store signals, typed route params — real type safety.
If you know Vue's provide/inject, React's hooks, or Solid's signals — you'll feel right at home. The best ideas, unified.
User-provided strings are inserted via textContent. URI components are encoded. Built-in security from day one.
These demos simulate how Nix.js signals, computed values, and effects work. Interact with them to see fine-grained reactivity.
Under the hood, Nix.js is a four-layer stack. Each layer does exactly one job — signal, compute, bind, render.
Reading a signal inside effect() or html`` automatically registers a
subscription. No .subscribe() calls, no decorator, no annotation needed.
Each reactive expression inside html`` compiles to exactly one effect(). When the
signal changes, that one effect updates that one text node or attribute — nothing else.
Before each re-run, an effect disposes its previous subscriptions and runs its cleanup function (if any). Unmounting a component tears down every effect it owns.
Setting a signal to the same value it already holds is a no-op. No downstream effects are triggered, no DOM work happens — not even a microtask.
Multiple signal writes inside batch() queue their effects until the batch ends. All
subscribers see a consistent snapshot, and the DOM updates exactly once.
Read a signal with untrack() to get its value without creating a subscription. Useful for
reading config or context inside an effect you don't want to re-trigger.
Clean, readable code that does exactly what you'd expect. No magic, no surprises.
Create reactive values with signal(),
derive with computed(),
and watch with effect().
Three primitives power the entire framework.
import { signal, computed, effect } from "@deijose/nix-js"; // Reactive state const count = signal(0); const doubled = computed(() => count.value * 2); // Auto-runs when count changes effect(() => { console.log(`Count: ${count.value}`); console.log(`Doubled: ${doubled.value}`); }); count.value = 5; // logs: Count: 5, Doubled: 10 // Batch multiple writes — effect runs once batch(() => { count.value = 10; count.update(n => n + 1); });
Function components for pages and display. Class components when you need lifecycle hooks. Both work seamlessly together.
// Function component — simple & clean function Counter(): NixTemplate { const count = signal(0); return html` <p>${() => count.value}</p> <button @click=${() => count.value++}> +1 </button> `; } // Class component — with lifecycle class Clock extends NixComponent { time = signal(new Date().toLocaleTimeString()); onMount() { const id = setInterval(() => { this.time.value = new Date() .toLocaleTimeString(); }, 1000); return () => clearInterval(id); } render() { return html`<span>${() => this.time.value}</span>`; } }
No extra package. Switch between history or hash mode, attach typed route meta, restore scroll automatically, and keep dynamic params, guards, and lazy loading.
import { createRouter, RouterView, Link, lazy } from "@deijose/nix-js"; const router = createRouter([ { path: "/", component: () => HomePage() }, { path: "/about", component: () => AboutPage() }, { path: "/dashboard", component: () => new DashboardLayout(), meta: { requiresAuth: true }, children: [ { path: "/stats", component: lazy( () => import("./pages/Stats")) }, { path: "/settings", component: lazy( () => import("./pages/Settings")) }, ], }, { path: "*", component: () => NotFound() }, ], { mode: "hash", scrollBehavior: (_to, _from, saved) => saved ?? { left: 0, top: 0 } }); // Auth guard using route meta from resolve() router.beforeEach((to) => { const match = router.resolve(to); if (match?.meta?.requiresAuth && !isAuth()) return "/login"; });
Every property becomes a signal automatically. Add typed actions and derived getters, subscribe globally to changes, and reset with $reset().
import { createStore, computed } from "@deijose/nix-js"; const cart = createStore( { items: [] as string[], total: 0, }, (s) => ({ add(item: string) { s.items.update(arr => [...arr, item]); s.total.update(n => n + 1); }, remove(item: string) { s.items.update(arr => arr.filter(i => i !== item)); s.total.update(n => n - 1); }, clear() { cart.$reset(); }, }), (s) => ({ itemCount: computed(() => s.items.value.length), hasItems: computed(() => s.items.value.length > 0), }) ); cart.$subscribe((key, next, prev) => { console.log("Store change:", key, prev, "→", next); }); cart.add("Milk"); cart.itemCount.value; // 1 cart.hasItems.value; // true
Manage complex forms with nested objects, cross-field rules, and dynamic arrays. Validation is fully typed and works with built-ins, custom validators, or schemas.
import { createForm, nixFieldArray, required, email, minLength } from "@deijose/nix-js"; const form = createForm({ profile: { email: "" }, password: "", confirmPassword: "" }, { validateOn: 'blur', validators: { "profile.email": [required(), email()], password: [required(), minLength(8)], confirmPassword: [ required(), (value, allValues) => value !== allValues?.password ? "Passwords do not match" : null ] } }); // Dynamic field array const { fields, append, remove } = nixFieldArray( [{ value: "" }], { value: [required(), email()] } ); const onSubmit = form.handleSubmit(values => { console.log("Form submit:", values, fields.value.length); });
Build with a minimal reactive core and scale with first-party tools like Nix Query, Nix Ionic, and Nix UI without dependency roulette.
Built-in field validation, dynamic arrays, and Zod/Valibot interop. Now includes nixFieldArray
for dynamic lists.
const form = createForm(
{ name: "", email: "" },
{ validators: {
name: [required(), minLength(2)],
email: [required(), email()],
}}
);
Render modals, tooltips, and toasts outside the component tree. Supports outlet tokens, refs, and provide/inject.
const modal = portal(
html`<div class="modal">
<h2>Confirm action</h2>
<button @click=${close}>OK</button>
</div>`
);
Catch render and reactive errors gracefully. Show fallback UIs without crashing the entire application.
createErrorBoundary(
new DataWidget(),
(err) => html`
<p class="error">
Failed: ${String(err)}
</p>`
);
CSS class-based enter/leave animations. No wrapper elements, JS hooks for full control, appear on first render.
transition(
() => show.value
? html`<p>Hello!</p>`
: null,
{ name: "fade", appear: true }
);
suspend() for async views, lazy() for code-splitting, and Nix Query for robust async requests, retries, and query cache invalidation.
suspend(
() => fetch("/api/users").then(r => r.json()),
(users) => html`
<ul>${users.map(u =>
html\`<li>${u.name}</li>\`
)}</ul>`,
{ invalidate: refreshKey }
);
Vue-style provide/inject with typed keys. Pass data down the tree without prop drilling. Nearest ancestor wins.
const THEME = createInjectionKey<
Signal<string>
>("theme");
provide(THEME, signal("dark"));
const theme = inject(THEME);
Built for real app workflows: command modes, retries, optimistic updates, and offline replay with a custom queue adapter.
@deijose/nix-query is CQRS-style state orchestration for Nix.js:
import { createCommand, CommandQueuedError, getQueryData, setQueryData } from "@deijose/nix-query"; const saveOrder = createCommand("orders/save", async (payload, { signal }) => { const res = await fetch("/api/orders", { method: "POST", body: JSON.stringify(payload), signal }); if (!res.ok) throw new Error("save failed"); return res.json(); }, { mode: "queueOffline", invalidate: ["orders/list"], retry: (count, err) => count < 3, retryDelay: (count) => Math.min(500 * 2 ** (count - 1), 5000), onMutate: (item) => { const prev = getQueryData("orders/list") ?? []; setQueryData("orders/list", [...prev, item]); return { prev }; }, onError: (_e, _item, ctx) => setQueryData("orders/list", ctx?.prev ?? []), offline: { adapter: myQueueAdapter, // implements CommandQueueAdapter isOnline: () => navigator.onLine, replayOnReconnect: true, maxReplayAttempts: 5 } } ); try { await saveOrder.executeAsync({ id: "A-100", total: 42 }); } catch (e) { if (e instanceof CommandQueuedError) { // queued offline; replay happens later } } await saveOrder.replayQueue();
We benchmarked Nix.js with 1,000-row scenarios using the js-framework-benchmark style workflow, reporting both JS-only and full-render timing so results are easier to interpret.
| Operation (1k rows) | Nix.js 1.3.0 | Nix.js 2.3.0 🚀 | Vanilla JS | Solid.js | Svelte 5 | Vue 3 | React 18 | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| JS | Full | JS | Full | JS | Full | JS | Full | JS | Full | JS | Full | JS | Full | |
| Create rows Initial render |
220.2ms
|
603.9ms
|
21.83ms WIN
|
109.84ms
|
~55ms
|
~80ms
|
~65ms
|
~130ms
|
~100ms
|
~180ms
|
~130ms
|
~280ms
|
~160ms
|
~350ms
|
| Replace rows Full array swap |
286.5ms
|
567.5ms
|
29.99ms WIN
|
121.01ms
|
~55ms
|
~85ms
|
~70ms
|
~140ms
|
~105ms
|
~190ms
|
~135ms
|
~290ms
|
~165ms
|
~360ms
|
| Update (1 in 10) Fine-grained text update |
0.8ms
|
40.1ms
|
0.21ms TOP
|
31.66ms
|
~4ms
|
~15ms
|
~5ms
|
~20ms
|
~8ms
|
~30ms
|
~12ms
|
~45ms
|
~15ms
|
~55ms
|
| Select row Highlight 1 element |
0.3ms
|
21.6ms
|
0.02ms TOP
|
30.62ms
|
~2ms
|
~8ms
|
~3ms
|
~12ms
|
~5ms
|
~18ms
|
~8ms
|
~28ms
|
~10ms
|
~35ms
|
| Swap rows Swap index 2 and 998 |
53.3ms
|
380.5ms
|
0.86ms TOP
|
31.18ms
|
~5ms ★
|
~20ms
|
~8ms
|
~30ms
|
~12ms
|
~45ms
|
~25ms
|
~90ms
|
~30ms
|
~110ms
|
| Clear rows Range.deleteContents() |
43.2ms
|
307.5ms
|
15.31ms WIN
|
31.85ms
|
~30ms
|
~50ms
|
~35ms
|
~60ms
|
~45ms
|
~75ms
|
~80ms
|
~150ms
|
~95ms
|
~180ms
|
| Delete row Eliminar 1 fila |
1.9ms
|
44.8ms
|
0.76ms TOP
|
26.03ms
|
~1ms
|
~5ms
|
~2ms
|
~8ms
|
~3ms
|
~12ms
|
~8ms
|
~25ms
|
~10ms
|
~35ms
|
| Gzipped Size Library footprint |
~10 KB
v1.3.0
|
~12 KB WIN
Router + Stores included
|
0 KB ★
Browser Native
|
~7 KB
Core only
|
~2 KB*
Requires compiler
|
~22 KB
Core + Runtime
|
~45 KB
React + DOM
|
|||||||
| Feature | Nix.js | React | Vue | Solid | Svelte |
|---|---|---|---|---|---|
| Router | Built-in ✓ | react-router | vue-router | @solidjs/router | svelte-kit |
| Form Validation | Built-in ✓ | react-hook-form | vee-validate | — | — |
| Global Stores | Built-in ✓ | zustand / redux | pinia | Built-in ✓ | svelte/store |
| Dependency Injection | Built-in ✓ | React Context | Built-in ✓ | createContext | getContext |
| Portals | Built-in ✓ | Built-in ✓ | Teleport ✓ | Built-in ✓ | — |
| Error Boundaries | Built-in ✓ | Built-in ✓ | errorHandler | Built-in ✓ | — |
| Transitions | Built-in ✓ | — | Built-in ✓ | — | Built-in ✓ |
Nix.js didn't emerge in a vacuum. It distills battle-tested ideas from the frameworks that shaped modern UI development — taking what works, discarding the overhead.
Lit pioneered the idea of using JavaScript's native tagged template literals to define HTML
templates — no compiler, no JSX, no virtual DOM. Nix.js adopts this exact approach: the html`` tag parses templates once and wires live
bindings directly to real DOM nodes.
Solid.js proved that signal-based fine-grained reactivity doesn't need a virtual DOM — just
wire effects directly to DOM nodes. Nix.js adopts the same reactive core: signal(), computed(), and effect() are the three primitives that power
everything.
Vue 3's Composition API introduced provide/inject, watch(), and typed lifecycle hooks as first-class
citizens. Nix.js mirrors this exactly: typed injection keys via createInjectionKey(), watch() with immediate/once options, and onMount / onUnmount hooks.
React proved that function components with colocated state are more composable than
class-only patterns. Nix.js supports both: function components (plain functions + html``, zero boilerplate) and class components
(NixComponent) only when lifecycle hooks are
needed.
Svelte's built-in transition: directive made
animations a first-class concern — without a separate animation library. Nix.js's transition() brings the same mental model: CSS
class-based enter/leave lifecycle with optional JS hooks and appear on first render.
MobX introduced transparent reactive tracking — read a value inside a reaction, and you're
automatically subscribed, no boilerplate. S.js formalized this into a dependency graph with batch() and untrack(). Nix.js inherits both: effects
auto-track their dependencies and untrack()
lets you opt out selectively.
The best frameworks aren't built from scratch — they're built on the shoulders of great ideas. Nix.js studies what works across the ecosystem and brings it together: tagged templates from Lit, fine-grained signals from Solid, provide/inject from Vue, function components from React, CSS transitions from Svelte, and transparent auto-tracking from MobX — unified into a single, zero-dependency, compiler-free package that respects your time and your bundle size.
Nix-Ionic bridges Nix.js reactivity with the full Ionic component library. Build native-quality mobile apps with signals, client-side routing, and modular loading. Since v1.x, only 6 routing-critical Ionic elements are registered by default, and the rest is imported on demand.
Install @deijose/nix-ionic
and call setupNixIonic().
Only import the components your app actually uses — the bundler tree-shakes everything else.
import { setupNixIonic, IonRouterOutlet } from "@deijose/nix-ionic"; import { NixComponent, html, mount } from "@deijose/nix-js"; // Import only the bundles you need import { layoutComponents } from "@deijose/nix-ionic/bundles/layout"; import { formComponents } from "@deijose/nix-ionic/bundles/forms"; setupNixIonic({ components: [...layoutComponents, ...formComponents], }); // Ionic router — uses ion-router under the hood const outlet = new IonRouterOutlet([ { path: "/", component: (ctx) => new HomePage(ctx) }, { path: "/task/:id", component: (ctx) => new TaskDetailPage(ctx) }, ]); class App extends NixComponent { render() { return html`<ion-app>${outlet}</ion-app>`; } } mount(new App(), "#app");
Register only the Ionic components your app uses. The runtime initializes a minimal routing core first, then you add bundles (or individual components) so the bundler tree-shakes the rest.
Layout, Navigation, Forms, Lists, Feedback, Buttons, Overlays, and All. Import by category or mix & match individual components.
First-class package.json exports let bundlers
resolve only the chunks you import. Full tree-shaking by design.
Every page and component uses Nix.js signals. Form inputs, tab states, modal toggles — all reactive without extra wiring.
ion-router, ion-route, ion-router-outlet, and ion-back-button are always registered. Deep linking and history-api navigation work out of the box.
Wrap the output with Capacitor to deploy as a real iOS or Android app. Same codebase, same signals, native shell.
We developed a full-featured mobile app to manage their community: member registration, route and event coordination, internal communication, and administrative tools—all from the phone.
Discover how the community is leveraging the Nix.js ecosystem to build high-performance UIs without the complexity.
A real-time cryptocurrency tracking dashboard demonstrating fine-grained reactivity, live data fetching, and dynamic UI updates.
Visit project
An academic tracking platform showcasing client-side routing, global state management, and nested layouts powered by Nix.js.
Visit project
High-performance task management with real-time stats, reactive stores, and advanced filtering with debounce.
Visit project@deijose/nix-query and @deijose/nix-ionic.
Chart.js, Leaflet,
and AG Grid without wrappers. If it runs in browser JavaScript, you can integrate it.
@deijose/nix-query for async requests, query cache, retries, and invalidation.
It is platform-agnostic and works in both web and mobile stacks. Start with:
npm install @deijose/nix-js @deijose/nix-query.
@deijose/nix-ionic@1.2.1 with Ionic Core for routing + native-style UI, then wrap
with Capacitor for Android/iOS deployment using the same codebase.
Build web apps with Nix.js + Nix-UI, or ship mobile apps with Nix-Ionic. Add Nix Query as the same async/cache layer in either path.
Nix.js is an open-source project built by developers, for developers. Whether it's a bug report, a feature request, or a pull request, your contribution matters.