NthJS is a lightweight, dependency-free javascript reactivity and reactive dom library. It provides fine-grained reactivity without a build step, making it an minimal alternative to frameworks like Vue or Svelte for smaller projects. It is great companion for PHP projects. It is ~1.4kb minified+gzipped.
NthJS provides a minimal yet solid base to make your own reactive library according to your needs. Just edit nthjs.js
file, This is <250 loc. Remove functions/features you don't want or add functions/features you want, such as automatic binding of data attributes etc.
I needed simple reactivity many time in frontend, but didn’t want the overhead of a build step. Neither I wanted to use Vue or other those have many features I never needed. I wanted something lightweight, yet powerful - something that took inspiration from Vue and Svelte, including features like watchers and group bindings, without unnecessary bloat. That’s why nthJS is born. It’s minimal, modular, and designed to keep reactivity purely in JavaScript, avoiding extra markup in the DOM. While it focuses on a JS-first approach, it can be easily extended to support data-attribute-based bindings if needed.
- Signals & Effects: Reactive state with automatic dependency tracking.
- Computed Values: Derived state that updates when dependencies change.
- Watchers: Listen for state changes and trigger callbacks.
- DOM Bindings: Bind state to the DOM with
createBinding
,createListBinding
, andcreateGroupBinding
. - Async Support: Fetch data reactively with
createAsyncSignal
.
Simply include the nthjs.js
file in your project via cdn.
<script src="https://cdn.jsdelivr.net/gh/nabeelalihashmi/nthjs@main/nthjs.min.js"></script>
or download and use.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NthJS Demo</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f4f4f4;
text-align: center;
}
h1 {
color: #333;
}
.container {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
display: inline-block;
}
.counter {
font-size: 24px;
margin: 10px 0;
}
button {
padding: 10px 15px;
margin: 5px;
font-size: 16px;
cursor: pointer;
border: none;
border-radius: 5px;
transition: 0.3s;
}
button:hover {
background: #ddd;
}
ul {
list-style: none;
padding: 0;
}
li {
padding: 8px;
background: #ddd;
margin: 5px 0;
border-radius: 5px;
}
.loading {
color: orange;
}
.error {
color: red;
}
</style>
</head>
<body>
<h1>NthJS Demo</h1>
<div class="container">
<!-- Counter -->
<h2>Counter</h2>
<div class="counter" id="count">0</div>
<button id="inc">Increment</button>
<button id="dec">Decrement</button>
<!-- Input Binding -->
<h2>Name Input</h2>
<input type="text" id="nameInput">
<p>Hello, <span id="nameDisplay"></span>!</p>
<!-- Computed Signal -->
<h2>Sum of Two Numbers</h2>
<input type="number" id="num1" value="0"> +
<input type="number" id="num2" value="0">
<p>Sum: <span id="sum"></span></p>
<!-- List Rendering -->
<h2>Dynamic List</h2>
<button id="addItem">Add Item</button>
<ul id="list"></ul>
<!-- Group Binding -->
<h2>Favorite Color</h2>
<label><input type="radio" name="color" value="red"> Red</label>
<label><input type="radio" name="color" value="blue"> Blue</label>
<label><input type="radio" name="color" value="green"> Green</label>
<p>Selected Color: <span id="selectedColor"></span></p>
<h2>Hobbies</h2>
<label><input type="checkbox" value="Reading"> Reading</label>
<label><input type="checkbox" value="Gaming"> Gaming</label>
<label><input type="checkbox" value="Cooking"> Cooking</label>
<p>Selected Hobbies: <span id="hobbies"></span></p>
<!-- Async Signal -->
<h2>Fetch Data</h2>
<button id="fetchData">Load Data</button>
<p id="loading" class="loading" style="display: none;">Loading...</p>
<p id="error" class="error"></p>
<p id="dataDisplay"></p>
</div>
<script src="https://cdn.jsdelivr.net/gh/nabeelalihashmi/nthjs@main/nthjs.min.js"></script>
<script>
// Counter
const count = createSignal(0);
createBinding(document.getElementById("count"), count);
document.getElementById("inc").addEventListener("click", () => count.value++);
document.getElementById("dec").addEventListener("click", () => count.value--);
// Name Input
const name = createSignal("User");
createBinding(document.getElementById("nameInput"), name, "value");
createBinding(document.getElementById("nameDisplay"), name);
// Computed Sum
const num1 = createSignal(0);
const num2 = createSignal(0);
const sum = createComputed(() => parseInt(num1.value) + parseInt(num2.value));
createBinding(document.getElementById("sum"), sum);
document.getElementById("num1").addEventListener("input", e => num1.value = e.target.value);
document.getElementById("num2").addEventListener("input", e => num2.value = e.target.value);
// Dynamic List
const items = createSignal([]);
createListBinding(document.getElementById("list"), items, item => {
const li = document.createElement("li");
li.textContent = item.text;
return li;
});
document.getElementById("addItem").addEventListener("click", () => {
items.value = [...items.value, { key: Date.now(), text: "Item " + (items.value.length + 1) }];
});
// Group Binding (Radio)
const selectedColor = createSignal("red");
createGroupBinding(document.querySelectorAll("input[name=color]"), selectedColor);
createBinding(document.getElementById("selectedColor"), selectedColor);
// Group Binding (Checkbox)
const hobbies = createSignal([]);
createGroupBinding(document.querySelectorAll("input[type=checkbox]"), hobbies);
createBinding(document.getElementById("hobbies"), hobbies);
// Async Signal (Fixed)
const [dataSignal, fetchData, isLoading, errorSignal] = createAsyncSignal(null, async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1');
if (!res.ok) throw new Error("Failed to fetch data!");
return res.json();
});
createEffect(() => {
document.getElementById("loading").style.display = isLoading.value ? "block" : "none";
document.getElementById("error").textContent = errorSignal.value || "";
document.getElementById("dataDisplay").textContent = dataSignal.value ?
`To-Do: ${dataSignal.value.title}` : "";
});
document.getElementById("fetchData").addEventListener("click", fetchData);
</script>
</body>
</html>
Creates a reactive signal.
initialValue
: Any value (primitive or object).
A signal object with:
.value
: Getter & setter for the reactive state.
const count = createSignal(0);
console.log(count.value); // 0
count.value = 5;
console.log(count.value); // 5
Runs an effect whenever its dependencies change.
effectFn
: A function that depends on reactive signals.
const count = createSignal(0);
createEffect(() => {
console.log("Count changed:", count.value);
});
count.value = 10; // Logs "Count changed: 10"
Creates a computed value that updates when dependencies change.
computeFn
: A function returning a derived value.
A signal containing the computed value.
const a = createSignal(2);
const b = createSignal(3);
const sum = createComputed(() => a.value + b.value);
console.log(sum.value); // 5
a.value = 10;
console.log(sum.value); // 13
Watches a signal and executes a callback when its value changes.
signal
: The signal to watch.callback
: Function(newValue, oldValue) => {}
.
const name = createSignal("Alice");
watch(name, (newVal, oldVal) => {
console.log(`Name changed from ${oldVal} to ${newVal}`);
});
name.value = "Bob"; // Logs: "Name changed from Alice to Bob"
Binds a signal to a DOM element.
element
: The target DOM element.signal
: The reactive signal.attribute?
: Optional attribute to bind.
const title = createSignal("Hello");
const h1 = document.querySelector("h1");
createBinding(h1, title); // Updates text content
const input = document.querySelector("input");
createBinding(input, title, "value"); // Two-way binding
Binds a signal array to a container element, rendering child elements.
container
: Parent element.signal
: Reactive array signal.renderItem
: Function(item) => HTMLElement
.
const items = createSignal([{ key: "1", text: "Item 1" }]);
const ul = document.querySelector("ul");
createListBinding(ul, items, (item) => {
const li = document.createElement("li");
li.textContent = item.text;
return li;
});
items.value = [...items.value, { key: "2", text: "Item 2" }];
Binds a group of radio buttons or checkboxes to a signal.
elements
: Array of input elements.signal
: Reactive signal.
For radio buttons:
const selected = createSignal("option1");
const radios = document.querySelectorAll("input[type=radio]");
createGroupBinding(radios, selected);
For checkboxes:
const selectedValues = createSignal(["a"]);
const checkboxes = document.querySelectorAll("input[type=checkbox]");
createGroupBinding(checkboxes, selectedValues);
Creates a reactive signal that updates from an async function.
initialValue
: Default value.fetcher
: Async function(signal) => Promise<data>
.
const [data, fetchData, loading, error] = createAsyncSignal(null, async () => {
const response = await fetch("https://api.example.com/data");
return response.json();
});
createEffect(() => {
if (loading.value) console.log("Loading...");
if (data.value) console.log("Data loaded:", data.value);
if (error.value) console.log("Error:", error.value);
});
// Trigger fetch
fetchData();
- No build step: Works directly in the browser.
- Tiny & fast: Minimal overhead, designed for simplicity.
- Fine-grained reactivity: Updates only the necessary parts of the DOM.
MIT