Copied!
v1.7.3 — Dynamic Forms & Enhanced State

Build reactive UIs
without the bloat.

A signal-based micro-framework. No virtual DOM, no compiler, no build-time magic. Just signals and tagged templates. Performance-first reactivity.

$ npm create nix-app@latest
TypeScript-first MIT License 221 tests passing ~10 KB gzipped Zero dependencies
~10 KB
Gzipped bundle
-70%
Faster Rendering (v1.7.3)
221
Tests passing
100%
TypeScript typed

From zero to reactive
in three steps.

No compiler, no config files, no boilerplate. Just install, write, and go.

Install

Add to your project

One package, zero runtime dependencies. Works with Vite, Webpack, or directly via ESM CDN.

# npm
$ npm install @deijose/nix-js

# or via ESM CDN (no install)
import { signal } from
  "https://esm.sh/@deijose/nix-js@1.7.3";
Create

Write your component

A plain function returning html`` is all you need. No class, no decorator, no JSX transform.

import { signal, html } from "@deijose/nix-js";

function App() {
  const count = signal(0);
  return html`
    <p>${() => count.value}</p>
    <button @click=${() => count.value++}>
      Click me
    </button>
  `
;
}
Mount

Render to the DOM

Call mount() once. Every signal update after this happens automatically — no re-render calls, no manual DOM updates.

import { mount } from "@deijose/nix-js";

// index.html: <div id="app"></div>

mount(App(), "#app");

// That's it. The app is live. ✓
No tsconfig.json, no vite.config.ts, no babel.config.js required — run it straight from the browser with an import map.

Everything you need,
nothing you don't.

A complete UI framework that fits in a single import. No virtual DOM overhead, no compiler step, no configuration files.

Fine-Grained Reactivity

Signals update only the exact DOM nodes that depend on changed data. No diffing, no reconciliation, no wasted renders.

No Compiler Required

Templates are standard JavaScript tagged template literals. No JSX transform, no SFC compiler, no build-time magic needed.

Batteries Included

Router, forms, stores, dependency injection, portals, error boundaries, transitions — all built-in. One import, zero config.

TypeScript Native

Every API is fully typed from the ground up. Typed injection keys, typed store signals, typed route params — real type safety.

Familiar Patterns

If you know Vue's provide/inject, React's hooks, or Solid's signals — you'll feel right at home. The best ideas, unified.

XSS Hardened

User-provided strings are inserted via textContent. URI components are encoded. Built-in security from day one.

See the reactivity
in action.

These demos simulate how Nix.js signals, computed values, and effects work. Interact with them to see fine-grained reactivity.

signal() + computed() Live
const count = signal(0);
const doubled = computed(
  () => count.value * 2
);
const label = computed(
  () => count.value === 0
    ? "zero"
    : count.value > 0 ? "positive" : "negative"
);
📋 repeat() + signal() Live
const todos = signal([]);
const remaining = computed(
  () => todos.value
    .filter(t => !t.done.value).length
);

html`<ul>${() =>
  repeat(todos.value, ...)
</ul>`;
🕐 effect() + lifecycle Live
class Clock extends NixComponent {
  time = signal("");

  onMount() {
    const id = setInterval(() => {
      this.time.value = new Date()
        .toLocaleTimeString();
    }, 1000);
    return () => clearInterval(id);
  }
}

One change. One update.
Zero overhead.

Under the hood, Nix.js is a four-layer stack. Each layer does exactly one job — signal, compute, bind, render.

signal()
Holds a reactive value. Notifies subscribers on write.
🔗
computed()
Derives a value. Re-runs only when its dependencies change.
🔄
effect()
Auto-tracks reads. Re-runs when any tracked signal changes.
🧩
html``
Parses once. Each binding is one effect targeting one DOM node.
🖥️
DOM Node
Only the exact node that changed gets updated. No diffing.
Subscription is automatic

Reading a signal inside effect() or html`` automatically registers a subscription. No .subscribe() calls, no decorator, no annotation needed.

🔁 Effect = DOM binding

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.

🧹 Self-cleaning effects

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.

🎯 Object.is equality

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.

📦 batch() flushes once

Multiple signal writes inside batch() queue their effects until the batch ends. All subscribers see a consistent snapshot, and the DOM updates exactly once.

🔒 untrack() for reads

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.

Write less, do more.

Clean, readable code that does exactly what you'd expect. No magic, no surprises.

Signals that just work.

Create reactive values with signal(), derive with computed(), and watch with effect(). Three primitives power the entire framework.

  • Automatic dependency tracking
  • Object.is equality — no wasted updates
  • Batch multiple writes into one flush
  • Self-cleaning effects with auto-disposal
  • untrack() for reading without subscribing
counter.ts
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);
});

Two styles. Your choice.

Function components for pages and display. Class components when you need lifecycle hooks. Both work seamlessly together.

  • Function components — zero boilerplate
  • Class components — lifecycle hooks
  • Children & named slots
  • DOM refs with ref()
  • Auto-cleanup on unmount
components.ts
// 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>`;
  }
}

Client-side routing, built in.

No extra package. History API router with dynamic params, nested routes, query strings, guards, and lazy loading — ready to go.

  • Dynamic route parameters
  • Nested routes with RouterView depth
  • Navigation guards (global + per-route)
  • Lazy loading with lazy()
  • Reactive active-link styling
router.ts
import { createRouter, RouterView, Link, lazy }
  from "@deijose/nix-js";

const router = createRouter([
  { path: "/",     component: () => HomePage() },
  { path: "/about", component: () => AboutPage() },
  {
    path: "/dashboard",
    component: () => new DashboardLayout(),
    children: [
      { path: "/stats",    component: lazy(
        () => import("./pages/Stats")) },
      { path: "/settings", component: lazy(
        () => import("./pages/Settings")) },
    ],
  },
  { path: "*", component: () => NotFound() },
]);

// Auth guard — redirect if not logged in
router.beforeEach((to, from) => {
  if (to.startsWith("/dashboard") && !isAuth())
    return "/login";
});

Global state in 5 lines.

Every property becomes a signal automatically. Add typed actions, reset to initial state with $reset(). No reducers, no dispatchers.

  • Auto-signalized properties
  • Typed actions with full inference
  • $reset() to restore initial state
  • Works with computed() and effect()
  • Use from any component or module
store.ts
import { createStore } 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(); },
  })
);

cart.add("Milk");
cart.items.value;   // ["Milk"]
cart.total.value;   // 1

Typed Forms & Dynamic Arrays.

Manage complex forms with ease. Built-in validation, dynamic field arrays, and enhanced state tracking for submission and dirty states.

  • Typed field validation (Zod/Valibot)
  • useFieldArray for dynamic lists
  • validateOn: 'blur' | 'input' | 'submit'
  • isSubmitting & submitCount tracking
  • Deeply reactive form state
forms.ts
import { createForm, useFieldArray, required, email } 
  from "@deijose/nix-js";

const { form, handleSubmit } = createForm({
  name: "",
  emails: ["test@example.com"]
}, {
  validateOn: 'blur',
  validators: { name: [required()] }
});

// Dynamic field array for emails
const { fields, append, remove } = useFieldArray(
  form.emails, 
  { validators: [required(), email()] }
);

const onSubmit = handleSubmit(data => {
  console.log("Form Submitting...", data);
});

One framework.
Everything included.

Stop juggling third-party packages. Nix.js ships with every feature you need to build production applications.

📋

Form Management

Built-in field validation, dynamic arrays, and Zod/Valibot interop. Now includes useFieldArray for dynamic lists.

const form = createForm( { name: "", email: "" }, { validators: { name: [required(), minLength(2)], email: [required(), email()], }} );
🔀

Portals

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>` );
🛡️

Error Boundaries

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>` );

Transitions

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 } );

Async & Suspense

suspend() for data fetching, createQuery() for shared queries, lazy() for code-splitting. Smart invalidation without DOM rebuild.

suspend( () => fetch("/api/users").then(r => r.json()), (users) => html` <ul>${users.map(u => html\`<li>${u.name}</li>\` )}</ul>`, { invalidate: refreshKey } );
💉

Dependency Injection

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);

Raw speed. Zero compiler.

We measured Nix.js against the industry standards using the official 1,000-row stress tests. The result? Top-tier performance that rivals—and sometimes beats—hand-optimized Vanilla JS.

JS ONLY Framework overhead only
FULL RENDER JS + Layout + Paint
Operation (1k rows) Nix.js 1.3.0 Nix.js 1.7.3 🚀 Vanilla JS Solid.js Svelte 5 Vue 3 React 18
JSFull JSFull JSFull JSFull JSFull JSFull JSFull
Create rows Initial render
220.2ms
603.9ms
20.3ms WIN
100.6ms
~55ms
~80ms
~65ms
~130ms
~100ms
~180ms
~130ms
~280ms
~160ms
~350ms
Replace rows Full array swap
286.5ms
567.5ms
26.2ms WIN
110.8ms
~55ms
~85ms
~70ms
~140ms
~105ms
~190ms
~135ms
~290ms
~165ms
~360ms
Update (1 in 10) Fine-grained text update
0.8ms
40.1ms
0.4ms TOP
31.3ms
~4ms
~15ms
~5ms
~20ms
~8ms
~30ms
~12ms
~45ms
~15ms
~55ms
Select row Highlight 1 element
0.3ms
21.6ms
0.1ms TOP
23.2ms
~2ms
~8ms
~3ms
~12ms
~5ms
~18ms
~8ms
~28ms
~10ms
~35ms
Swap rows Swap index 2 and 998
53.3ms
380.5ms
15.6ms
93.0ms
~5ms
~20ms
~8ms
~30ms
~12ms
~45ms
~25ms
~90ms
~30ms
~110ms
Clear rows Range.deleteContents()
43.2ms
307.5ms
17.2ms WIN
33.1ms
~30ms
~50ms
~35ms
~60ms
~45ms
~75ms
~80ms
~150ms
~95ms
~180ms
Delete row Eliminar 1 fila
1.9ms
44.8ms
0.9ms TOP
27.7ms
~1ms
~5ms
~2ms
~8ms
~3ms
~12ms
~8ms
~25ms
~10ms
~35ms
Gzipped Size Library footprint
~10 KB
v1.3.0
~10 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
* Averages calculated from a base of 20 distinct samples per operation mode to rule out V8 GC outliers. Based on official js-framework-benchmark methodology. Lower times are better.

Built-in vs. Third-party

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 ✓

Inspired by the best.
Refined into one.

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
google.github.io/lit
Tagged Templates

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.

// Lit's html tag (the original idea)
import { html } from 'lit';
html`<p>Hello ${name}</p>`;

// Nix.js takes the same approach
import { html } from '@deijose/nix-js';
html`<p>${() => name.value}</p>`;
lit.dev
Solid.js
solidjs.com
Fine-Grained Signals

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.

// Solid.js reactive primitives
const [count, setCount] = createSignal(0);
createEffect(() => console.log(count()));

// Nix.js — same concept, unified API
const count = signal(0);
effect(() => console.log(count.value));
solidjs.com
Vue 3
vuejs.org
Composition API

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.

// Vue 3 — provide / inject
provide('theme', ref('dark'));
const theme = inject('theme');

// Nix.js — typed keys
const THEME = createInjectionKey<Signal<string>>('theme');
provide(THEME, signal('dark'));
vuejs.org
React
react.dev
Hooks Model

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.

// React — function component + state
function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n+1)}>{n}</button>;
}

// Nix.js — no JSX, no compiler
function Counter(): NixTemplate {
  const n = signal(0);
  return html`<button @click=${() => n.value++}>${() => n.value}</button>`;
}
react.dev
Svelte
svelte.dev
CSS Transitions

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.

// Svelte — transition directive
<div transition:fade>Hello!</div>

// Nix.js — same idea, no compiler
transition(
  () => show.value ? html`<p>Hello!</p>` : null,
  { name: 'fade', appear: true }
);
svelte.dev
MobX & S.js
Observer pattern
Auto-tracking

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.

// Nix.js batch + untrack — from MobX/S.js
batch(() => {
  price.value = 20;  // writes queued
  qty.value = 3;    // effect runs once
});

effect(() => {
  const a = price.value;     // tracked
  const b = untrack(() => qty.value); // not
});
mobx.js.org

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.

— Design philosophy of Nix.js

Built with Nix.js

Discover how the community is leveraging the Nix.js ecosystem to build high-performance UIs without the complexity.

Common questions,
straight answers.

Still have questions?

Browse the full documentation or open an issue on GitHub. The community is small but growing fast.

Open an issue on GitHub
Yes — Nix.js is plain JavaScript at runtime. TypeScript is optional. You get full type safety when using TS, but the library works identically in a plain .js or even directly in a browser <script type="module"> tag via an import map.
Nix.js has 221 passing tests covering the full public API: reactivity, templates, router, stores, typed forms, dynamic arrays, DI, portals, error boundaries, and transitions. It ships as a strict TypeScript library with no runtime dependencies. It is a great fit for small-to-medium apps, embedded widgets, and internal tools. For large enterprise apps, evaluate it on your use case.
Yes. Nix.js is a standard ES module and works with any bundler that supports ESM — Vite, Webpack 5, Rollup, Parcel, esbuild, Bun. It also runs without a bundler in modern browsers via type="importmap" and https://esm.sh/@deijose/nix-js.
Those are great choices for most projects. Choose Nix.js when you want fine-grained reactivity without a compiler or a virtual DOM, when bundle size matters (React is ~45 KB gzipped, Vue ~33 KB), or when you want routing, forms, stores, DI, portals, and transitions from one import with no configuration. It's also ideal when you want to understand or extend a framework without fighting a complex compiler.
Nix.js is a client-side framework and does not currently support SSR. It targets browser environments where the DOM is available. If you need server-side rendering, consider pairing a static HTML shell with Nix.js for client-side hydration, or using a full-stack framework. SSR support is on the long-term roadmap.
The "Vanilla JS" baseline in standard benchmarks relies on typical patterns (like removeChild loops) which can trigger multiple internal browser reflows. Nix.js automates extreme low-level DOM optimizations for you. For instance, it uses Range.deleteContents() for atomic bulk clearing, and holds direct memory references to TextNodes for true O(1) updates. It writes the most optimal Vanilla JS possible, automatically.
Absolutely. We believe in total transparency and reproducibility. You can run the exact same 1,000-row stress tests directly in your own browser using our live interactive benchmark tool.

Run the live benchmarks here →

Ready to build
something great?

Start building reactive UIs in minutes. No compiler, no config, no learning curve — just install and go.