Nux β tiny core. total control.
Nux is a zero-dependency, TypeScript-first micro-framework for DOM components.
- β Build isolated components with lifecycle and refs.
- β»οΈ Destroy components with a single line.
- π‘ Use in Astro, static HTML, or any SSR stack.
The name Nux combines several meaningful layers that reflect the core qualities of the framework:
- Nux as new UX β emphasizing the creation of a modern, flexible, and enhanced user experience layer for web applications.
- Nux as nucleus β symbolizes the core, the origin from which everything else is born. Minimalistic and foundational architecture that kickstarts UI development.
- Nux as nut β small, compact, yet incredibly powerful. A framework with enormous potential packed into a tiny size.
Thus, Nux is a lightweight, basic, yet powerful and extensible tool that energizes and structures your frontend components.
- β‘οΈ Zero Dependencies β Lightweight core, no external runtime packages.
- π§ Fully Typed API β Built entirely with TypeScript. Intellisense included.
- π§© Component-Based Architecture β Isolated logic with lifecycle, actions, and state.
- π― Direct DOM Control β Fine-grained event binding with scoped data-refs and native API.
- π₯ Scoped Event System β Nux leverages the native browser EventTarget API (no polyfills, full performance).
- β»οΈ Clean Destroy Logic β One-liner teardown of listeners, timeouts, or observers.
- π Framework-Agnostic β Works with Astro, SSR, or plain HTML.
- π Zero Runtime Overhead β No hydration or reactivity overhead unless you need it.
- π¦ Tiny Footprint β Under 1KB when minified and gzipped.
-
Install
npm install @zoxon/nux
-
Import the library in your JavaScript or TypeScript file:
import { Component, defineComponent } from '@zoxon/nux'
-
Create a component by extending the
Component
class:import { Component } from '@zoxon/nux' class Counter extends Component { count = 0 init() { this.get('increment')?.addEventListener('click', () => { this.count++ this.get('output')!.textContent = String(this.count) }) } }
-
HTML Markup
<div data-component="counter"> <button data-ref="increment">Increment</button> <span data-ref="output">0</span> </div>
-
Register and Initialize
import { defineComponent, initComponents } from '@zoxon/nux' defineComponent('counter', Counter) initComponents({ scope: 'page' })
Every component extends the Component base class, giving you lifecycle methods and utilities to interact with the DOM and other components in a clean, consistent way.
init()
: Called automatically when the component is initialized.buildCache()
: Called during construction to process refs and perform pre-binding setup.bindEvents()
: Bind all DOM and custom events here. Called after buildCache().destroy()
: Optional cleanup method called when the component is destroyed.
export class MyComponent extends Component {
async init() {
console.log('Component is live!')
}
buildCache() {
this.title = this.get('title')
}
bindEvents() {
this.title?.addEventListener('click', () => alert('Clicked!'))
}
destroy() {
console.log('Component removed')
}
}
get(name: string): HTMLElement | undefined
- Returns a single element by data-ref
. Should be prefixed with component name in markup.
If multiple components may be on the page, prefix data-ref
with the component name (e.g., data-ref="modal:title"
). Otherwise, plain data-ref="title"
is fine.
<div data-ref="modal:title">Hello</div>
const title = this.get('title')
getAll(name: string): HTMLElement[]
- Returns an array of elements by data-ref
. Should be prefixed with component name in markup.
<div data-ref="modal:button">Button 1</div>
<div data-ref="modal:button">Button 2</div>
const buttons = this.getAll('button')
Property | Type | Description |
---|---|---|
name |
string |
The name of the component. |
element |
HTMLElement |
The root DOM element. |
options |
Record<string, any> |
Optional config passed to the component. |
data |
Record<string, any> |
Custom data passed to the component. |
rawRefs |
Reference[] |
Raw references parsed from DOM. |
Your components can talk to each other using any event system like @zoxon/eventor
β a lightweight, type-safe event system built on top of native custom events.
-
Install:
npm install @zoxon/eventor
-
Update the events map type:
declare interface WindowEventMap { 'modal:show': CustomEvent<{ id: string }> 'modal:close': CustomEvent<{ id: string }> }
-
Listen an event:
import { Component } from '@zoxon/nux' import { listenEvent } from '@zoxon/eventor' export class Modal extends Component { close() { listenEvent('modal:close', (event) => { const { id } = event.detail // Handle the event, e.g., close the modal with the given id }) listenEvent('modal:show', (event) => { const { id } = event.detail // Handle the event, e.g., show the modal with the given id }) } }
-
Dispatch an event:
import { Component } from '@zoxon/nux' import { dispatchEvent } from '@zoxon/eventor' export class SomeComponent extends Component { showModal(id: string) { const button = this.get<HTMLButtonElement>('button') if (button) { button.addEventListener('click', () => { dispatchEvent('modal:show', { id }) }) } } }
<div data-component="counter">
<div data-ref="counter:result"></div>
<button type="button" data-ref="counter:plus">+</button>
<button type="button" data-ref="counter:minus">-</button>
</div>
import { Component, defineComponent } from '@zoxon/nux'
// Extend Counter class
interface Counter {
plusButton?: HTMLButtonElement
minusButton?: HTMLButtonElement
result?: HTMLElement
counter: number
}
class Counter extends Component {
// Init class variables
buildCache() {
this.plusButton = this.get<HTMLButtonElement>('plus')
this.minusButton = this.get<HTMLButtonElement>('minus')
this.result = this.get<HTMLElement>('result'
6B74
)
this.counter = 0
}
bindEvents() {
if (this.plusButton) {
this.plusButton.addEventListener('click', () => {
this.counter++
this.renderResult()
})
}
if (this.minusButton) {
this.minusButton.addEventListener('click', () => {
this.counter--
this.renderResult()
})
}
}
// Render result once
init() {
this.renderResult()
}
renderResult() {
if (this.result) {
this.result.innerHTML = this.counter.toString()
}
}
}
// Init component on element with data-component="counter" attribute
defineComponent('counter', Counter)
We welcome PRs, issues, and ideas! Open a discussion or submit a patch β letβs build a better component model together.
MIT Β© Konstantin Velichko