Zero-build, zero-dependency, standalone, reactive, lightweight UI framework. ~23KB minified (~8KB gzipped).
Documentation - Examples - LLM Reference (~2,900 tokens) - npm
<script src="https://cdn.jsdelivr.net/npm/@maleta/blokjs/dist/blokjs.min.js"></script>
<div id="app"></div>
<script>
blok.mount('#app', {
state: { count: 0 },
methods: {
inc() { this.count++ },
dec() { this.count-- },
},
view: ($) => ({
div: { children: [
{ h1: { text: $.count } },
{ button: { click: 'dec', text: '-' } },
{ button: { click: 'inc', text: '+' } },
] }
})
})
</script>No virtual DOM. Views are plain JavaScript objects. State is reactive - when count changes, the bound h1 updates automatically.
- Reactive state - fine-grained dependency tracking via ES Proxy
- Components - props, events, slots, lifecycle hooks
- Routing - client-side router with params, guards, and history/hash modes
- Stores - global state with computed properties and async tracking
- Async tracking - automatic
loadinganderrorstate for async methods - URL sanitization -
javascript:and dangerous URIs blocked on href/src attributes - Zero dependencies - single script tag, no build step needed
blok.component('TodoItem', {
props: ['todo'],
methods: {
remove() { this.emit('remove', this.todo) }
},
view: ($) => ({
li: { children: [
{ input: { type: 'checkbox', model: $.todo.done } },
{ span: { text: $.todo.text } },
{ button: { click: 'remove', text: 'x' } },
] }
})
})Use components by name in templates. Pass props, listen to events with on_ prefix:
{ each: $.todos, as: 'todo', key: 'id', children: [
{ TodoItem: { todo: $.todo, on_remove: 'handleRemove' } }
] }Global state shared across components. Async methods get automatic loading/error tracking:
blok.store('auth', {
state: { user: null },
computed: {
isLoggedIn() { return this.user !== null }
},
methods: {
async login(email, password) {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
this.user = await res.json()
},
logout() { this.user = null }
}
})// In templates
{ when: $.store.auth.loading.login, children: [
{ p: 'Signing in...' }
] }
{ when: $.store.auth.error.login, children: [
{ p: { class: 'error', text: $.store.auth.error.login } }
] }
// In methods
this.store.auth.login(email, pw)
this.store.auth.isLoggedInblok.mount('#app', {
routes: [
{ path: '/', component: 'Home' },
{ path: '/product/:id', component: 'ProductDetail' },
{ path: '/admin', component: 'Admin', guard: 'requireAuth' },
{ path: '*', component: 'NotFound' },
],
guards: {
requireAuth(to, from) {
if (!this.store.auth.isLoggedIn) return '/login'
return true
}
},
view: ($) => ({
div: { children: [
{ a: { href: '/', link: true, text: 'Home' } },
{ a: { href: '/admin', link: true, text: 'Admin' } },
{ div: { route: true } },
] }
})
})Access route data in components via this.route.params, this.route.query, and navigate programmatically with this.navigate('/path').
Views are plain objects. The $ proxy creates reactive references resolved at render time.
// Conditionals
{ when: $.isLoggedIn, children: [{ p: 'Welcome!' }] }
{ when: $.not.isLoggedIn, children: [{ p: 'Please log in' }] }
// Loops
{ each: $.items, as: 'item', key: 'id', children: [
{ li: { text: $.item.name } }
] }
// Two-way binding
{ input: { type: 'text', model: $.search } }
{ select: { model: $.category, children: [...] } }
// Classes (string, object, or array)
{ div: { class: { active: $.isActive, disabled: $.isOff } } }
// Events
{ button: { click: 'save', text: 'Save' } }
{ button: { click: 'remove(item)', text: 'x' } }
{ form: { submit: 'handleSubmit', children: [...] } }
// Slots
{ Card: { title: 'Hello', children: [
{ p: 'This goes into the slot' }
] } }While BlokJS works without a build step, the Vite plugin adds automatic component and store registration from file directories, bundling into optimized output, and hot module replacement during development.
Scaffold a new project:
npm create blokjs my-app
cd my-app
npm install
npx viteOr add to an existing Vite project:
npm install blokjs vite-plugin-blokjs// vite.config.js
import { defineConfig } from 'vite'
import { blokjs } from 'vite-plugin-blokjs'
export default defineConfig({
plugins: [blokjs()],
})// main.js
import { mount } from '@maleta/blokjs'
import 'virtual:blokjs'
mount('#app', {
view: ($) => ({ counter: {} }),
})The plugin auto-discovers files in components/ and stores/ directories, registering each by filename. Files prefixed with _ are ignored.
components/counter.js -> component('counter', ...)
stores/app.js -> store('app', ...)
components/_helper.js -> ignored
BlokJS supports all ES2020-compliant browsers (96%+ global coverage). IE is not supported.
pnpm install
pnpm build # build all packages
pnpm test # run testsMIT