📚 Full documentation is available at docs.nordjs.dev.
Nord is a lightweight, reactive JavaScript framework for building single-page applications. Its design is determined by simplicity, performance, and developer-first ergonomics.
- Nord is a JavaScript library for building reactive single-page applications.
- Nord is dependency-free, with zero third-party runtime dependencies.
- Nord embraces TypeScript, but does not require it.
- Nord is a pure runtime framework, and can run entirely without build tools.
- Nord embraces modern DX tools, like Vite, even though they are optional.
- Nord is well tested, with high coverage across its core primitives.
- Nord is fast, competitive with other frameworks
- Nord uses grains (signals) as reactive primitives, but does not enforce them. Any subscribable that exposes a
subscribefunction can be used. - Nord surgically updates DOM nodes based on grain changes — no re-rendering, virtual DOM, or diffing involved.
- Nord utilizes a component-based architecture, enabling composition through reactive building blocks.
- Nord abstracts DOM logic, but exposes it via directives for advanced control.
- Nord provides first-class modules for HTTP, forms, routing, and internationalization (i18n).
- Nord commits to a minimal core runtime, under 10KB, extendable through optional modules.
- Nord is completely SSR ready
- Nord is performant by default, utilizing stable templates and fine-grained updates to eliminate rerendering entirely.
- Nord utilizes tagged template literals instead of JSX, keeping it dependency-free and standards-aligned.
- Nord provides modern devtools, including an IDE extensions for an elevated development experience.
Modern frontend development has drifted far from the browser. Frameworks reinvent what browsers already do well - reactivity, templating, styling - then ship megabytes of JavaScript to recreate it. Nørd takes the opposite approach: work with the browser, not against it. A minimal API surface is all that's needed.
import { html, mount } from '@grainular/nord';
const App = () => html`Hello World`;
mount(App, { to: document.querySelector('#main') });
// Or in one line
mount(() => html`Hello World`, { to: document.querySelector('#main') });import { html, mount } from '@grainular/nord';
const Child = () => {
return html`I'm a child Component.`;
};
const App = () => {
return html`<div>${Child()}</div>`;
};
mount(App, { to: document.querySelector('#main') });import { html, mount } from '@grainular/nord';
const Child = ({ name }: { name: string }) => {
return html`Hello ${name}`;
};
// OR typed
const Child: PureComponent<{ name: string }> = ({ name }) => {
return html`Hello ${name}`;
};
const App = () => {
return html`<div>${Child({ name: 'World' })}</div>`;
};
mount(App, { to: document.querySelector('#main') });import { html, mount } from '@grainular/nord';
import { withStyles, css } from '@grainular/styled';
const Button = ({ label }) => {
return withStyles(
() => html`<button>${label}</button>`,
() => css`
/* Scoped to this component */
button {
background: red;
color: white;
}
`,
);
};
mount(() => Button({ label: 'Click Me' }), {
to: document.querySelector('#app'),
});import { html, mount } from '@grainular/nord';
const App = () => {
console.log('Rendered');
return html`Hello World`;
};
mount(App, { to: document.querySelector('#main') });
// Logs `Rendered` only once.import { html, mount, on } from '@grainular/nord';
import { grain } from '@grainular/grains';
const App = () => {
const count = grain(0); // inferred as `grain<number>`
const handleClick = () => count.set(count() + 1);
return html` <button ${on('click', handleClick)}>${count}</button>`;
};
mount(App, { to: document.querySelector('#main') });
// Renders a <button>0</button>
// After clicking
// Renders a <button>1</button>import { html, mount, on } from '@grainular/nord';
import { grain } from '@grainular/grains';
export const globalCount = grain(0); // Can be used anywhere
const App = () => {
// Can be used inside the component and it's children
const count = grain(0);
const handleClick = () => count.set(count() + 1);
return html` <button ${on('click', handleClick)}>${count}</button>`;
};
mount(App, { to: document.querySelector('#main') });
// Renders a <button>0</button>
// After clicking
// Renders a <button>1</button>import { html, mount, on } from '@grainular/nord';
import { grain } from '@grainular/grains';
type CounterProps = {
count: Grain<number>;
};
const Counter = ({ count }: CounterProps) => {
const increment = () => count.set(count() + 1);
return html` <button ${on('click', increment)}>${count}</button>`;
};
const App = () => {
const count = grain(0); // Can be used inside the component and it's children
const reset = () => count.set(0);
return html` ${Counter({ count })}
<button ${on('click', reset)}>Reset</button>`;
};
mount(App, { to: document.querySelector('#main') });import { html, mount } from '@grainular/nord';
const Child = ({ children }: PropsWithChildren) => {
return html`I'm a child Component. ${children}`;
};
const App = () => {
return html`<div>${Child({ children: 'Some Text' })}</div>`;
};
mount(App, { to: document.querySelector('#main') });import { html, mount } from '@grainular/nord';
const Child = ({ children }: PropsWithChildren) => {
return html`I'm a child Component. ${children}`;
};
const App = () => {
// You can define fragments whereever you want, they are normal values
// that can be used throughout your app.
const childTemplate = html`<div>Some markup</div>`;
return html`<div>${Child({ children: childTemplate })}</div>`;
};
mount(App, { to: document.querySelector('#main') });import { html, mount } from '@grainular/nord';
const App = () => {
const condition = grain(true);
return html` <div>
<!-- Comment. Not visible in the DOM -->
</div>`;
};
mount(App, { to: document.querySelector('#main') });import { html, mount, on } from '@grainular/nord';
const App = () => {
const handleClick = () => console.log('Clicked');
return html` <button ${on('click', handleClick)}>0</button>`;
};
mount(App, { to: document.querySelector('#main') });
// Renders a <button>0</button>import { html, mounted, mount } from '@grainular/nord';
const App = () => {
console.log('Rendered');
return html` <div ${mounted((node) => console.log({ node }))}>Hello World</div>`;
};
mount(App, { to: document.querySelector('#main') });
// Logs `Rendered`
// Logs the node after the node is inserted and connected.import { html, mount, $if } from '@grainular/nord';
const App = () => {
const condition = grain(true);
return html` <div>
${$if(condition)
.$then(() => html`Boolean is true`)
.$else(() => html`Boolean is False`)}
</div>`;
};
mount(App, { to: document.querySelector('#main') });import { html, mount, $switch } from '@grainular/nord';
export const App = () => {
const count = grain(0);
const type = grain<'a' | 'b' | 'c'>('a');
return html` <div>
${$switch(type)
.$case('a', () => html`<div>A</div>`)
.$case('b', () => html`<div>B</div>`)
.$case('c', () => html`<div>C</div>`)
.$default('Not the right type')}
</div>`;
};
mount(App, { to: document.querySelector('main#app') });import { html, mount, $await } from "@grainular/nord"
export const App = () => {
const promise = new Promise<string>((res) => {
setTimeout(() => res('Hello World'), 2000)
})
return html`
<div>
${$await(promise)
.$then((data) => html`${data}`)
.$catch((err) => html`${err.message}`)
.$pending(() => html`Loading...`)
)}
</div>`
}
mount(App, { to: document.querySelector('main#app') })import { html, mount, $each } from "@grainular/nord"
export const App = () => {
const users = grain([{ name: 'A', age: 2 }, { name: 'B', age: 20 }]);
return html`<div>
${$each(users)
.$as(({name, age}, idx, arr) => html`
<div>
Name: ${name},
Age: ${age}
</div>`
)
)}
</div>`
}
mount(App, { to: document.querySelector('main#app') })import { html, mount, mounted, ref, createRef } from '@grainular/nord';
export const App = () => {
const div = createRef<HTMLDivElement>(); // creates a ref<HtmlElement> grain
console.log(div.current); // logs null
return html` <div ${ref(div)} ${mounted(() => console.log(div.current))}></div>`;
};
mount(App, { to: document.querySelector('main#app') });
// Logs: HTMLDivElementimport { html, mount, createDirective } from '@grainular/nord';
// Creating a directive your self is simple and typesafe
const color = (color: string) =>
createDirective((node) => {
node.style.backgroundColor = color;
});
export const App = () => {
return html` <div ${color('red')}>I'm a red div</div>
<div ${color('blue')}>I'm a blue div</div>`;
};
mount(App, { to: document.querySelector('main#app') });import { html, mount, syncReactive, on } from '@grainular/nord';
import { BehaviorSubject } from 'rxjs';
const subject = new BehaviorSubject(0);
const count = syncReactive({
get: () => subject.value,
subscribe: (subscriber) => subject.subscribe(subscriber),
});
export const App = () => {
return html`<button ${on('click', () => subject.next(count() + 1))}>${count}</button>`;
};
mount(App, { to: document.querySelector('main#app') });import { createCustomElement } from '@grainular/custom-elements';
import { html } from '@grainular/nord';
import { grain } from '@grainular/grains';
const Counter = () => {
const count = grain(0);
const handleClick = () => count.set(count() + 1);
return html` <button ${on('click', handleClick)}>
${count}
<button></button>
</button>`;
};
export default createCustomElement(() => Counter(), { selector: 'nord-counter' });