One state
window._ is the whole state. The assignment itself makes it reactive, and _.mount() is ready to go.
HTML-first reactive micro-framework in a single file, no dependencies, no build. Inspired by Fixi, it rewrites the essence of Petite-Vue.
window._ is the whole state. The assignment itself makes it reactive, and _.mount() is ready to go.
When _.user.name changes, only the nodes that read it update. The whole tree is never re-rendered.
Package reusable functionality with <define-component>: typed props, local state and SFC files.
A single file, included with one tag:
Then define the state and mount:
Assigning window._ already makes it reactive and injects the
helpers, so _.mount() is ready right after.
Every directive starts with : and its value is a JavaScript expression evaluated with _ as context.
| Directive | Use |
|---|---|
:text | Inserts escaped text |
:: | Syntactic sugar equivalent to :text |
:html | Inserts unescaped HTML |
:html-unsafe | Same as :html — a marker for consciously inserting raw HTML |
:model | Two-way binding with forms |
:as | Type coercion for :model (int, number, bool, array, object) |
:transition | View Transition on re-render; modifiers .fade, .scale, .slideup, .shared="name" |
:if | Conditional rendering (on <template>) |
:else | Else branch with :if (on <template>) |
:each | Repeats content (on <template>) |
:ref | References an element in _.ref |
:on.* | Listens to events. this is the element |
:is | Dynamic root element on component usage (<x-card :is="article">) |
:attribute | Any HTML attribute (:class, :src, :value…) |
:shorthand | :name without = expands to :name="name" |
If :if or :each appear on a non-<template> element, the console will warn with the problem so you can fix it.
Full documentation in docs/api.md.
Available depending on context:
_ global state
In events: this is the element and event the event
In :each loops, item is the iterated element, item._index its position, and item._parent the parent loop if any.
There are no $el, $refs, $nextTick, $parent or $root.
The whole JavaScript API boils down to two lines:
window._ wraps it in a reactive Proxy and injects the helpers right away._.mount() scans the DOM; it is idempotent and re-scanable: after inserting dynamic HTML just call _.mount(container).MutationObserver: you decide when to mount again._mount() injects a few minimal one-liner helpers into _ (like _.ref): only if you have not defined them yourself, and as non-enumerable properties (they don't pollute Object.keys(_) or :each iterations). Because _ is the prototype of the expression context, they are usable as bare identifiers in any directive.
| Helper | What it does |
|---|---|
$(sel) | document.querySelector(sel) |
$$(sel) | [...document.querySelectorAll(sel)] |
net.get(url) | fetch(url).then(r => r.text()) |
net.json(url) | fetch(url).then(r => r.json()) |
net.post(url, data) | POST JSON → text |
dispatch(name, detail) | CustomEvent (bubbles, composed) on document |
sleep(ms) | new Promise(r => setTimeout(r, ms)) (for await) |
raf(fn) | requestAnimationFrame(fn) |
mount(el?) | Mounts (re-mounts) el; accepts a selector or element |
dispatch is the complement to :on.myevent: it fires a custom event that any :on.* listener (or addEventListener) can catch. Inside a :on.* handler, this is the element, so this.dispatchEvent(new CustomEvent(...)) targets the element itself.
Menut lets you package reusable functionality into single-file components (SFC), declared with <define-component> and used as custom elements. The reactive core does not change: a component instance is a mount() against its local state.
The definition is explicit and separate from usage: <define-component> leaves the component registered and ready to be used later by its tag.
A .html file with a single <template> (identical to the inline one):
The tag is derived from the filename (components/x-counter.html → x-counter), so the name must contain a hyphen. It is loaded with fetch() + DOMParser (which never runs scripts) and waiting instances are booted once it arrives.
shadowrootmode="open" to the <template> to encapsulate._ is local and the global is accessed as window._ (no automatic fallback to the global).<script>, this is the local reactive state; this.el is the element.:each creates one instance per row, and the element props bind with the parent context (<x-num :num="item.v">).They are declared on the <template> with name:type="default" attributes:
| Type | Coercion | Default |
|---|---|---|
:string | String(v) | "" |
:number | Number(v) | 0 |
:boolean | true unless "false" | false |
:date | new Date(v) | null |
:array | JSON.parse(v) | [] |
:object | JSON.parse(v) | {} |
| (no suffix) | auto-detect | as-is |
onpropchange fires when a prop changes, whether from the attribute or the state.In the <script> you can assign callbacks (all with this = state):
this.onpropchange = (name, val) => {} — when a prop changes.this.onconnected = () => {} — when the element connects (DOM ready).this.ondisconnected = () => {} — when it disconnects.mount() registers <define-component> blocks before scanning, so definitions work even when they come after their usages. On registration the <style> and <script> are extracted from the template content (not cloned per instance); the style is injected once with scope (tag { … } + :host→tag). Each instance boots with a local reactive state (props + script methods), clones the template, scans it against that state and inserts it into the light DOM.
<script> goes inside the <template>: that way it is inert at parse time and Menut extracts it at definition.:each.<script> is synchronous (no top-level await); it can touch the DOM via this.el (runs after the template is rendered), but geometry measurements are unreliable before the first paint (use requestAnimationFrame or measure on interaction). Prop names are lowercase.Full example in examples/components.html · Reference in docs/api.md.
Menut ships with a re-usable library of SFC components in components/. Browse them in the live gallery or the auto-generated reference, grouped by category: forms, data, text, buttons, layout, 3d, content.
To propose a new component: copy components/_template.html, fill its doc comment (@component, @category, @usage), add a demo to examples/components.html and open a pull request. A bot runs on the PR — the verify-component check, a static scan plus an AI review — and main is protected: the check must pass (and for community PRs, a review) before it can merge. New components are verified automatically, docs regenerate after merging, and the gallery is re-published from main.
Full guide with conventions, rules and checklist: CONTRIBUTING.md.
These pages also use Menut. Menut is mounted in this very document.
Everything you need to know about Menut lives in three documents, kept as the project's source of truth.
Philosophy, design decisions, internal architecture and code rules. The project's source of truth.
Full reference for every directive (::, :model, :each…) and the expression context.
Quick overview: what Menut is, installation and a complete 30-line example.
How to propose a component: SFC format, categories, conventions and the PR flow with its verification bot.
Why Menut exists, how it compares to the alternatives, and the trade-offs it chose.
Demos ready to open in the browser. They all use real Menut.
Single-file components with <define-component>: typed props, light DOM, and external loading via src.
Nested :each loops: albums with tracks, with item and item._index at each level.
Changing one branch does not re-render the other: each branch counts its DOM mutations.
With a loop of inputs, adding a row does not steal focus from what you are typing.
:transition with .scale, .fade, .slideup and .shared morphs.
::, :text, :on.click and :hidden.
:model, :each, item and item._index.
:each over tables, sorting and reactive tasks.length.
:model on text, checkbox, radio and select, and :html.
A self-contained suite that exercises every directive and shows the result on screen. Open it in any browser.
During development the same suite runs against menut.js and menut.min.js to check they are equivalent.
An interactive treemap of every function in menut.js, proportional to its size.
unsafe-evalMenut uses new Function() to compile HTML expressions into JavaScript at runtime. This requires script-src 'unsafe-eval' in Content Security Policy.
This was a deliberate trade-off: it keeps the framework tiny (~13 kb), the API simple (no build step), and the codebase readable. A CSP-strict variant is possible — see CSP compatibility for the full analysis.