Copied!
v2.1.0 — Runtime-hardened reactivity and DevTools sync fixes

Build reactive UIs in the browser
with no build step.

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.

$ npm create nix-app@latest
TypeScript-first MIT License 484 tests passing 95.86% Coverage ~12 KB gzipped Nix UI Nix Query 1.3.2 Zero dependencies
~12 KB
Gzipped bundle
-90%
Best-case JS-only gain
484
Tests passing
100%
TypeScript typed

The Lightweight
Micro-Framework.

We include the router, forms, and state management in a bundle smaller than other frameworks' core runtimes.

React + DOM
~42 KB
Vue 3
~34 KB
Nix.js (Total)
~12 KB

* Gzipped sizes including core + standard internal features.

Why trust this approach?

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.

Lit Templates like Lit, without Web Components overhead.
Solid.js Fine-grained reactivity for pure ESM projects.
Vue 3 The provide/inject pattern you love, zero-dep.
React Functional components without hook-rule headaches.
Svelte Reactive DX without needing a .svelte compiler.
MobX Transparent state tracking for any JS object.
View technical inspiration breakdown

Use DOM-first libraries
without wrappers.

Real integration pattern: use refs, lifecycle hooks, and cleanup exactly as you would in production code.

Bring your existing stack as-is

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.

Chart.js Visualizations
MapLibre Dynamic Maps
AG Grid Enterprise Grids
Any JS native DOM plugin
chart-integration.ts
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();
  }
}

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@2.3.0";
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.

Traditional Frameworks

  • Configure build tool (Vite/Webpack)
  • Set up Babel/SWC transpilation
  • Bundle compilation (Heavy artifacts)
  • Debug compiled/minified code

The Nix Way

  • Create index.html
  • Import via CDN or local ESM
  • Write pure JS templates
  • Debug exactly what you wrote

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() Native ESM 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. Switch between history or hash mode, attach typed route meta, restore scroll automatically, and keep dynamic params, guards, and lazy loading.

  • History + hash routing modes
  • Route meta available through resolve()
  • Custom scrollBehavior restoration
  • Nested routes with RouterView depth
  • Navigation guards and lazy loading
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(),
    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";
});

Global state in 5 lines.

Every property becomes a signal automatically. Add typed actions and derived getters, subscribe globally to changes, and reset with $reset().

  • Auto-signalized properties
  • Typed actions with full inference
  • Optional gettersFactory for derived signals
  • Global $subscribe(key, next, prev)
  • $reset() to restore initial state
  • Works in any component or module
store.ts
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

Typed Forms, Dot Paths & Cross-Field Rules.

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.

  • Typed field validation (Zod/Valibot)
  • Dot-path validators for nested fields
  • Cross-field validators with allValues
  • nixFieldArray for dynamic lists
  • validateOn: 'blur' | 'input' | 'submit'
  • isSubmitting & submitCount tracking
forms.ts
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);
});

One core.
Official packages.

Build with a minimal reactive core and scale with first-party tools like Nix Query, Nix Ionic, and Nix UI without dependency roulette.

📋

Form Management

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()], }} );
🔀

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

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

Nix Query goes beyond fetch + cache.
Queues, offline mode, and command orchestration.

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:

latest queue parallel queueOffline
  • createQuery for read operations with status/data/error signals.
  • createCommand for mutations with retries, dedupe, invalidation, and optimistic rollback.
  • Cache utilities like getQueryData, setQueryData, and updateQueryData.
Offline Queue + Replay Queue commands while offline and replay on reconnect.
CommandQueuedError Distinguish queued-offline from real command failures.
Optimistic Rollback Use onMutate/onError to keep UI fast and safe.
Retry + Backoff Fine-grained retry policy per command.
v1.3 includes offline queue controls: replayQueue() and clearQueue().
View Nix Query on npm
orders-command.ts
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();

Raw speed. Zero compiler.

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.

Read this before comparing numbers

  • 1. JS-only means framework/runtime cost only. Full render includes layout + paint.
  • 2. The create-row win can happen when DOM patching and batching reduce framework overhead in the measured phase.
  • 3. Use both columns before drawing conclusions for your real app workload.
JS ONLY Framework overhead only
FULL RENDER JS + Layout + Paint
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
* 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;     
// not tracked
  const b = untrack(() => qty.value);
});
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

Nix.js goes mobile.

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.

Ionic Web Components.
Nix.js Reactivity.

Install @deijose/nix-ionic and call setupNixIonic(). Only import the components your app actually uses — the bundler tree-shakes everything else.

  • Modular loading — import only what you need
  • Core bootstrap registers only 6 essential Ionic elements
  • Pre-made bundles: layout, forms, overlays, navigation…
  • Full Ionic routing with ion-router & ion-back-button
  • Compatible with Capacitor for true native deployment
  • Signals-first — all state is reactive by default
$ npm install @deijose/nix-ionic
Nix-Ionic 1.2.1 Nix.js 2.1.0 compatible Ionic Core 8.x
main.ts
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");

Modular Loading

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.

📦

8 Category Bundles

Layout, Navigation, Forms, Lists, Feedback, Buttons, Overlays, and All. Import by category or mix & match individual components.

🔗

Sub-path Exports

First-class package.json exports let bundlers resolve only the chunks you import. Full tree-shaking by design.

📡

Signals-First State

Every page and component uses Nix.js signals. Form inputs, tab states, modal toggles — all reactive without extra wiring.

🧭

Built-in Routing

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.

📲

Capacitor Ready

Wrap the output with Capacitor to deploy as a real iOS or Android app. Same codebase, same signals, native shell.

Available Bundles — v1.2.1

Tree-shakeable
layout
header, toolbar, title, content, footer, buttons
navigation
menu, menu-button, menu-toggle
forms
input, textarea, checkbox, radio, select, toggle, searchbar
lists
list, item, label, thumbnail, avatar, card
feedback
spinner, progress-bar, skeleton-text, badge, chip, note
buttons
button, fab, fab-button, fab-list, ripple-effect
overlays
modal, popover, toast, alert, action-sheet
all
Everything — backward-compatible, ~35 KB gz
Legacy migration guide (v0.2.x -> v1.x)
// Before — loaded all components automatically
- setupNixIonic();

// After — you choose what to load
+ import { allComponents } from "@deijose/nix-ionic/bundles/all";
+ setupNixIonic({ components: allComponents });

// Or go modular for the smallest possible bundle
+ import { layoutComponents } from "@deijose/nix-ionic/bundles/layout";
+ setupNixIonic({ components: [...layoutComponents, defineIonButton] });
Starter Template View on npm GitHub
Success Case

Iron Bikers: mobile app for biker community

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.

Mobile App Android & iOS Community Management Routes & Events
4 Weeks
Android & iOS
100% Custom Built
Iron Bikers App Login Iron Bikers App Light Mode Iron Bikers App Dark Mode

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
Nix.js 2.x focused on reliability: safer reactivity behavior across bundling scenarios, stronger DevTools synchronization, and a more mature ecosystem with dedicated packages like @deijose/nix-query and @deijose/nix-ionic.
Yes. It is designed for production with zero runtime dependencies in the core, strong TypeScript support, and a practical ecosystem for web and mobile workflows.
Yes. Nix.js works directly with DOM-first libraries like Chart.js, Leaflet, and AG Grid without wrappers. If it runs in browser JavaScript, you can integrate it.
Use @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.
Yes. Use @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.
Usually minimal. Most upgrades are dependency updates plus a quick validation pass over routes, reactive effects, and tooling integrations.
Yes. Run the same benchmark scenarios from the public playground: Open live benchmarks →

Two paths.
Web or mobile.

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.

Help us build the next generation of reactive UIs.

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.

Contribute on GitHub View good first issues