Built with intention. Designed for humans.
Houxit (pronounced HOW-zit) is a modern JavaScript framework for building reactive, component-driven web applications. It draws inspiration from the elegance of Vue.js while introducing a redesigned, more expressive API vocabulary — one where every name communicates what it actually does.
The name itself carries the philosophy: an exit from rigid conventions, toward a developer-first experience rooted in clarity, speed, and freedom.
Houxit is not trying to replace React, Vue, Angular, or Svelte by copying them. It's built on a different set of questions: What if the API said exactly what it meant? What if every piece had only one job, and that job was obvious?
Clarity over convention. Names in Houxit describe intent. initBuild initializes a build. token holds a reactive value you read with .data. stream gives you a live, reactive proxy of an object. model is your state. handlers are your methods. None of these require you to look up what they really do.
Granular reactivity. Houxit tracks dependencies at the property level. Only the precise parts of the DOM that depend on a changed value are updated — not the whole component tree.
Three authoring styles, one framework. You can write widgets as plain functions, as options objects, or as classes. You can use the declarative template syntax or drop to hyperscript render functions. You can write in a single .houxit file or compose from plain .js modules. Houxit meets you where you are.
- Widget-driven architecture — The UI is a tree of self-contained, reusable widgets.
- Granular reactivity — Updates target only the DOM nodes that actually changed.
- Declarative templates — HTML-superset syntax with directives, blocks, and interpolation.
- Two-way data binding — Keep inputs and state in sync effortlessly with
$$model. - Three authoring APIs — Options API, Adapter API (composition), and Class API.
- Built-in animation system —
$$animateand$$transitefor declarative transitions. - Slots & scoped content — Pass content into widgets with default, named, and scoped slots.
- TypeScript support — Full types out of the box.
- CDN or build-tool usage — No CLI required for simple use cases.
.houxitWidget Unit Files — Co-locate script, template, and styles in one file.
Here is a complete counter widget in a .houxit file:
<!-- Counter.houxit -->
<script build>
import { token } from 'houxit'
const count = token(0)
function increment() {
count.data++
}
</script>
<template>
<div>
<p>You have clicked {{ count }} times.</p>
<button @click="increment">Click me</button>
</div>
</template>
<style $$scoped>
button {
font-size: 1.2em;
padding: 0.4em 1em;
}
</style>
A few things to notice right away:
token(0)creates a reactive value. You writecount.data++to mutate it.- In the template,
{{ count }}— Houxit unwraps.dataautomatically. @clickis shorthand for$$on:click. Both work.$$scopedon<style>means the CSS only applies to this widget.
The fastest way to try Houxit — drop a script tag into any HTML file, no build step needed:
<!-- Development build -->
<script src="https://cdn.jsdelivr.net/npm/houxit/dist/houxit.global.js"></script>
<!-- Production build (minified) -->
<script src="https://cdn.jsdelivr.net/npm/houxit/dist/houxit.global.min.js"></script>With the global build, Houxit is available as the Houxit global:
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/houxit/dist/houxit.global.js"></script>
<script>
const { initBuild } = Houxit
initBuild({
model() {
this.message = 'Hello from Houxit!'
},
template: `<h1>{{ message }}</h1>`
}).mount('#app')
</script>
</body>You can also use the ES module build with an import map:
<script type="importmap">
{
"imports": {
"houxit": "https://cdn.jsdelivr.net/npm/houxit/dist/houxit.esm.js"
}
}
</script>
<script type="module">
import { initBuild } from 'houxit'
initBuild({ template: `<p>Hello!</p>` }).mount('#app')
</script>npm install houxitimport { initBuild } from 'houxit'For projects that use .houxit Widget Unit Files and a full build pipeline:
npm create houxit@latestFollow the prompts to name your project and select a preset: minimal, standard, or full. The scaffolded structure looks like this:
my-app/
├── src/
│ ├── widgets/
│ │ └── App.houxit
│ └── main.js
├── index.html
└── package.json
Start the dev server:
cd my-app
npm install
npm run devinitBuild is the entry point for every Houxit application. It accepts a root widget definition and returns an application instance.
import { initBuild } from 'houxit'
const app = initBuild({
model() {
this.count = 0
},
handlers: {
increment() {
this.count++
}
},
template: `
<div>
<p>Count: {{ count }}</p>
<button $$on:click="increment">+</button>
</div>
`
})
app.mount('#app')One special behavior: when Houxit mounts the root widget via initBuild and that widget has no template, render, or markdown option, it falls back to the innerHTML of the mount target as the template. This allows progressive enhancement of server-rendered HTML.
Resolution order for root widget output:
renderfunction (hyperscript)templateoption (string)markdownoption (markdown string)innerHTMLof the mount element (initBuild context only)
Everything in Houxit is a widget — a self-contained, reusable piece of UI that manages its own state, reacts to inputs, and produces rendered output.
Widgets communicate through clearly defined channels:
| Channel | Purpose |
|---|---|
$params |
Declared inputs passed from the parent |
$signals |
Declared events the widget emits |
$events |
Native DOM events that fall through |
$attrs |
Undeclared attributes passed from the parent |
A Houxit application is a tree of widgets, rooted at the widget passed to initBuild.
Houxit accepts widgets in three forms. All three are equally valid — use whichever fits your style or the complexity of the widget.
A function widget is treated directly as the build function. It runs once during initialization and must return a render function.
function Greeting({ params }) {
return () => <h1>Hello, {params.name}!</h1>
}This is the most minimal form — great for simple, stateless widgets.
A structured object with named options. This is the most readable form for widgets with moderate complexity.
export default {
model() {
this.count = 0
},
handlers: {
increment() {
this.count++
}
},
template: `<button $$on:click="increment">Clicked {{ count }} times</button>`
}Extend Houxit.Widget for an object-oriented authoring style.
class MyWidget extends Houxit.Widget {
constructor() {
super()
this.model = {
value: 'hello'
}
}
template = `<p>{{ value }}</p>`
}A Widget Unit File (WUF) is the .houxit single-file format. It lets you co-locate a widget's script, template, and styles in one file — keeping related concerns together without coupling them.
<!-- src/widgets/Counter.houxit -->
<script build>
import { stream } from 'houxit'
const user = stream({ name: 'Chukwuemeka', score: 0 })
function addPoint() {
user.score++
}
</script>
<template>
<div class="counter">
<p>{{ user.name }}: {{ user.score }} points</p>
<button @click="addPoint">+1</button>
</div>
</template>
<style>
.counter {
font-family: sans-serif;
padding: 1rem;
}
</style>
<script build> runs in the Adapter API context. Variables declared at the top level are automatically available in the template.
<script> (without build) uses the Options API — export a default widget definition object.
<style $$scoped> scopes CSS to this widget only. Without $$scoped, styles are global.
The Options API defines a widget as a plain object. Each key plays a specific, named role.
Declares the widget's reactive state. Define properties on this inside the function — Houxit reads them at instantiation, makes every property reactive, and merges them into the widget instance.
export default {
model() {
this.count = 0
this.user = { name: 'Chukwuemeka', role: 'admin' }
this.items = []
}
}In templates and handlers, state properties are accessed directly by name: count, user.name. In handlers, use this.count to mutate.
Why a function? Defining
modelas a function (rather than a plain object) ensures each widget instance gets its own independent copy of the state. If it were a plain object, all instances would share the same reference.
Declares the widget's accepted inputs. Params are passed by the parent and are read-only inside the widget. They're available in templates as $params.propName.
export default {
params: {
title: String,
count: {
type: Number,
default: 0
},
onSave: Function
}
}Replaces what other frameworks call "methods". These are functions that operate on the widget's state and are callable from the template.
export default {
handlers: {
increment() {
this.count++
},
async fetchData() {
this.items = await api.getItems()
}
}
}Replaces watchers. Keys are reactive state paths (or functions returning a value); values are callbacks that fire when the observed value changes.
export default {
observers: {
count(newVal, oldVal) {
console.log(`count changed from ${oldVal} to ${newVal}`)
},
'user.name'(newName) {
document.title = newName
}
}
}You can also observe a computed expression:
observers: {
// Watch a derived value
[() => this.items.length](newLen) {
console.log(`List now has ${newLen} items`)
}
}A string containing the widget's HTML output. Supports all Houxit template syntax.
export default {
template: `
<div>
<h1>{{ title }}</h1>
<p $$if="show">Visible</p>
</div>
`
}An explicit render function using the h hyperscript helper. When render is present, template and markdown are ignored.
import { h } from 'houxit'
export default {
render() {
return h('div', null,
h('h1', null, this.title),
h('p', null, `Count: ${this.count}`)
)
}
}Lifecycle hooks run at precise moments in a widget's life. Define them as functions directly in the options object.
| Hook | When it runs |
|---|---|
preBuild |
Before the widget is built/initialized |
postBuild |
After the widget is built |
init |
When the widget instance is first created |
preMount |
Just before the widget is inserted into the DOM |
postMount |
After the widget is mounted in the DOM |
preUpdate |
Before a reactive update causes a re-render |
postUpdate |
After a reactive update has re-rendered |
export default {
model() {
this.count = 0
},
postMount() {
console.log('Widget is in the DOM')
},
postUpdate() {
console.log('DOM updated')
}
}The Adapter API is the composition-oriented authoring style — used in <script build> blocks in WUF files, or called inside the build function option of the Options API. It exposes reactive primitives, lifecycle hooks, and widget configuration as importable functions that can be freely composed and reused across modules.
Important: Adapter API functions must be called synchronously during widget initialization — at the top level of
<script build>or directly insidebuild. They register themselves against the active widget instance, so calling them insidesetTimeout,asynccontinuations, or event handlers won't work.
token wraps a single reactive value — a number, string, boolean, or any primitive. Reading .data tracks the dependency; writing .data triggers updates.
import { token } from 'houxit'
const count = token(0)
count.data // read → 0
count.data = 5 // write → triggers updates
count.data++ // shorthand mutationIn templates inside <script build>, use the variable name directly — Houxit unwraps .data automatically:
<p>{{ count }}</p> <!-- renders the value, not the token object -->
<p>{{ count * 2 }}</p> <!-- expressions work too -->token can also hold a reference to a DOM element or widget instance. Use $$bind:ref to connect it:
const inputEl = token(null)<input $$bind:ref="inputEl" />After mount, inputEl.data points to the actual DOM node.
stream creates a deeply reactive proxy of an object or array. Unlike token, there is no .data accessor — the stream is the object. You read and write properties directly on it, as if it were the original:
import { stream } from 'houxit'
const user = stream({ name: 'Chukwuemeka', age: 30 })
user.name // read → 'Chukwuemeka'
user.name = 'Ike' // write → triggers updates
const items = stream([])
items.push('first') // array mutations are tracked too
tokenvsstream— when to use which: Usetokenfor a single primitive value (count,isOpen,title). Usestreamfor an object or array (user,settings,items). The key difference:tokenwraps its value inside.data;streamexposes the value directly as a proxy of the original object.
In templates, both are accessed by variable name — Houxit handles the rest.
createAgent creates a reactive getter/setter pair — ideal for sharing a piece of state across multiple widgets without prop-drilling.
import { createAgent } from 'houxit'
const [getCount, setCount] = createAgent(0)
getCount() // read → 0
setCount(5) // set directly
setCount(({ value }) => value + 1) // update from previous valueAgents work well with Provider for injecting shared state into a widget subtree without threading props through every level.
computed derives a reactive value from other reactive values. The function only re-runs when a dependency changes, and the result is cached between reads.
import { token, computed } from 'houxit'
const count = token(2)
const doubled = computed(() => count.data * 2)
doubled.data // → 4 (updates automatically when count changes)Think of computed as a smart, lazy-evaluated expression that stays in sync automatically. Use it whenever a value is derived from state rather than set directly.
observe watches a reactive source and fires a callback when it changes.
import { token, observe } from 'houxit'
const count = token(0)
observe(count, (newVal, oldVal) => {
console.log(`changed: ${oldVal} → ${newVal}`)
})
// Watch a computed expression
observe(() => count.data * 2, (doubled) => {
console.log('doubled:', doubled)
})Options:
observe(source, callback, {
initial: true, // run immediately with the current value
deep: true // deeply observe nested objects/arrays
})
observevseffectHook: Useobservewhen you want to react to a specific source changing. UseeffectHookwhen you want an effect to auto-track whatever reactive state it reads.
effectHook runs a callback immediately and re-runs it whenever any reactive state it reads changes. Houxit automatically tracks which reactive values were accessed during execution.
import { token, effectHook } from 'houxit'
const count = token(0)
effectHook(() => {
document.title = `Count is ${count.data}`
// Houxit tracks that this effect reads count.data
// and re-runs it whenever count changes
})Return a cleanup function to run before the next execution, or when the widget unmounts:
effectHook(() => {
const timer = setInterval(() => count.data++, 1000)
return () => clearInterval(timer) // cleanup
})import { token, stream, readonly, shallow, readonlyStream, shallowStream } from 'houxit'
// Expose a token as read-only to the outside world
const count = token(0)
const readCount = readonly(count) // reading works, writing throws
// Shallow stream: only reacts to top-level property changes
// (nested property mutations don't trigger updates)
const state = shallowStream({ nested: { value: 1 } })
// Readonly shallow
const locked = readonlyStream(state)Use readonly to enforce one-directional data flow — expose state without allowing mutation from outside the widget.
All Options API lifecycle hooks have Adapter API equivalents as importable functions. Call them at the top level of <script build> or inside build.
import {
preBuild, postBuild,
init,
preMount, postMount,
preUpdate, postUpdate
} from 'houxit'
postMount(() => {
console.log('mounted!')
})
postUpdate(() => {
console.log('DOM updated')
})Houxit's reactivity system is based on automatic dependency tracking. When a reactive value (token, stream, createAgent getter, or model property) is read during rendering or inside effectHook, Houxit records that dependency. When the value changes, Houxit knows exactly which parts of the DOM — or which effects — to re-run.
const user = stream({ name: 'Chukwuemeka', age: 30 })
effectHook(() => {
// This effect only re-runs when user.name changes.
// Changing user.age does nothing here.
console.log(user.name)
})This is what "granular reactivity" means in practice. Houxit doesn't re-render the whole widget — it re-runs only the effects and DOM nodes that read the changed value.
Mutations on a stream-wrapped array are tracked automatically:
const list = stream([1, 2, 3])
list.push(4) // triggers update
list.splice(0, 1) // triggers update
list[0] = 99 // triggers updateimport { token, computed } from 'houxit'
const price = token(100)
const tax = token(0.2)
const total = computed(() => price.data * (1 + tax.data))
total.data // → 120
// Update either dependency → total recalculates automaticallyThe build option in the Options API is Houxit's bridge between the two authoring styles. When you define build, it runs in the Adapter API context — you can call stream, token, observe, effectHook, lifecycle hooks, and any other Adapter API function inside it.
build receives the widget's context object as its first argument and must return a render function.
import { stream, effectHook, postMount, h } from 'houxit'
export default {
params: {
initialCount: Number
},
build({ $params }) {
const count = token($params.initialCount ?? 0)
postMount(() => {
console.log('Counter mounted with', count.data)
})
effectHook(() => {
document.title = `Count: ${count.data}`
})
function increment() {
count.data++
}
return () => h('div', null,
h('p', null, `Count: ${count.data}`),
h('button', { 'on:click': increment }, '+')
)
}
}Note:
thisis undefined insidebuild. All state lives in the function closure. This is intentional — it enforces clean, explicit data flow with no hidden instance context.
A plain function widget is treated as a build function directly:
function Counter({ params }) {
const count = token(params.initial ?? 0)
return () => h('div', null,
h('p', null, `${count.data}`),
h('button', { 'on:click': () => count.data++ }, '+')
)
}Houxit templates are a valid HTML superset. They compile to efficient render code at build time (or at runtime for CDN usage). Any valid HTML is valid in a Houxit template.
Use double curly braces to interpolate reactive expressions into text:
<p>Hello, {{ name }}!</p>
<p>2 + 2 = {{ 2 + 2 }}</p>
<p>{{ isLoggedIn ? 'Welcome back' : 'Please sign in' }}</p>Interpolation is text-only by default — HTML in the value is escaped for security. To render raw HTML, use the $$html directive or {{@html}} block.
Directives are special attributes prefixed with $$. They give Houxit instructions on how to treat a DOM element reactively. Most directives also have a shorthand syntax.
Conditionally render an element. The element and its children are fully created or destroyed.
<p $$if="isLoggedIn">Welcome back!</p>
<p $$else-if="isPending">Loading...</p>
<p $$else-if="isFailed">Something went wrong.</p>
<p $$else>Please log in.</p>
$$ifvs CSSdisplay: none:$$ifremoves the element from the DOM entirely. Use it when the content truly shouldn't exist. If you want the element to remain in the DOM but be invisible (preserving state or avoiding re-mount costs), use a style binding instead.
Render a list. Use of syntax with a *key for efficient reconciliation:
<ul>
<li $$for="item of items" *key="item.id">
{{ item.name }}
</li>
</ul>With index:
<li $$for="(item, key, index) of items" *key="index">
{{ index }}: {{ item.name }}
</li>Always provide
*key. The key tells Houxit which DOM node corresponds to which item when the list changes. Without it, Houxit can't reuse nodes efficiently and may produce incorrect behavior during reorders or deletions.
Bind a DOM attribute or property to a reactive expression. The * shorthand prefix is the idiomatic form:
<!-- Full syntax -->
<img $$bind:src="user.avatarUrl" $$bind:alt="user.name" />
<!-- Shorthand -->
<img *src="user.avatarUrl" *alt="user.name" />
<!-- Boolean attributes -->
<button *disabled="isLoading">Submit</button>
<!-- Two-way binding for inputs -->
<input $$model="message" />$$model is a special two-way binding directive for form elements. It keeps the element's value and the reactive state in sync automatically.
Attach an event listener. The @ shorthand is idiomatic:
<!-- Full syntax -->
<button $$on:click="increment">+</button>
<!-- Shorthand -->
<button @click="increment">+</button>
<!-- Inline expression -->
<button @click="count.data++">+</button>
<!-- Event modifiers -->
<form @submit|prevent="handleSubmit">...</form>
<!-- Multiple events -->
<div @click.hover|stop="select">...</div>Event modifiers (after |) modify how the event is handled: prevent calls event.preventDefault(), stop calls event.stopPropagation(), and so on.
Declares a slot outlet. The # shorthand is used on the parent side to target a named slot:
<!-- Inside a Card widget's template -->
<div class="card">
<div class="card__header">
<slot name="header" />
</div>
<div class="card__body">
<slot /> <!-- default slot -->
</div>
</div>
<!-- Using the Card widget -->
<Card>
<h2 #header>My Title</h2>
<p>Card body content.</p>
</Card>Houxit extends standard class and style binding with a dot-notation shorthand.
<!-- Object syntax -->
<div $$bind:class="{ active: isActive, disabled: isDisabled }"></div>
<!-- Array syntax -->
<div $$bind:class="[baseClass, isActive ? 'active' : '']"></div>
<!-- Dot-notation: applies 'name' and 'header' as static classes -->
<div class.name.header></div>
<!-- Bound dot-notation: applies both classes when isHeader is truthy -->
<div *class.name.header="isHeader"></div><!-- Object syntax -->
<p $$bind:style="{ color: textColor, fontSize: size + 'px' }"></p>
<!-- Dot-notation: sets a CSS property to a literal value -->
<p style.color="red"></p>
<!-- Bound dot-notation: evaluated as a JS expression -->
<p *style.color="primaryColor"></p>
<!-- Multiple properties from one expression -->
<p *style.color.background="isDark ? 'white' : 'black'"></p>Blocks are multi-line template constructs for control flow and utility beyond what single-attribute directives can express. They use the {{@keyword}} / {{/keyword}} delimiter syntax.
Some blocks are void blocks — they close automatically after the opening tag and ignore children or closing tags.
{{@if count > 10}}
<p>Count is large!</p>
{{@else:if count > 5}}
<p>Count is medium.</p>
{{@else}}
<p>Count is small.</p>
{{/if}}@else:if and @else are void blocks — they self-close. @else:if and @else are only valid inside an @if / /if pair.
{{@for item of items}}
<div class="item">
<h3>{{ item.title }}</h3>
<p>{{ item.body }}</p>
</div>
{{/for}}With index and key:
{{@for (item, i) of items}}
<li *key="item.id">{{ i + 1 }}. {{ item.name }}</li>
{{/for}}Declare a local template variable scoped to the nearest enclosing block (or the template root). @const is a void block.
{{@const total = items.reduce((s, i) => s + i.price, 0)}}
<p>Total: {{ total }}</p>
{{@const greeting = `Hello, ${user.name}!`}}
<h1>{{ greeting }}</h1>Use @const to name complex expressions once, avoiding repetition and making templates easier to read.
Render a raw HTML string. @html is a void block. Only use with trusted content — this bypasses Houxit's automatic HTML escaping.
{{@html article.bodyHtml}}Render async content inline. Best used inside a Suspense widget.
<Suspense>
<p #fallback>Loading...</p>
{{@await fetchData()}}
</Suspense>Suspense handles the pending state — while fetchData() hasn't resolved, the #fallback slot content is shown.
Advanced blocks for scenarios where class-based constructs are needed within the rendering logic.
{{@class Formatter=()}}
{{@const fmt = @new Formatter(locale)}}
<p>{{ fmt.format(value) }}</p>
{{/class}}Houxit has a built-in animation system with two directives:
$$transite— CSS transitions for elements entering or leaving the DOM (paired with$$ifor$$for).$$animate— Keyframe-style animations with full programmatic control.
<p $$if="show" $$transite:fade="{ delay: 300 }">Hello</p>Define the transition in your widget options:
export default {
transitions: {
fade(node, params) {
return {
keyframes: [
{ opacity: [0, 1], duration: 300 }, // enter
{ opacity: [1, 0], duration: 200 } // leave
]
}
}
}
}<div $$animate:spin>Loading...</div>export default {
animations: {
spin(node, { to, from }, params) {
return {
keyframes: [
{ transform: 'rotate(0deg)' },
{ transform: 'rotate(360deg)' }
],
duration: 1000,
iterations: Infinity
}
}
}
}When declaring animations in Adapter mode, use bracket syntax in the template to evaluate the animation name as a JS expression:
<div $$animate:[spin]>Loading...</div>
<div $$transite:[whizzz]="{ duration: 400 }">Fading...</div>To resolve a registered animation or transition function from an ancestor scope:
import { resolve } from 'houxit'
const fade = resolve.animation('fade')
function whizzz(node, params) {
// Custom transition — does not require registration
}In render functions, use the motion prop:
// JSX
<div motion={animate(whixx, {})} />
// Render function
h('input', { motion: transite(fade, {}) })Or use the attach prop for full control:
<div attach={({ animate, transite }) => {
transite(fly, {})
animate(whoosh)
}} />Motion is a declarative wrapper that manages enter/leave transitions for a group of elements — including text nodes, which are compiled to <span> elements.
<Motion name="slide" mode="out-in">
<div $$if="view === 'home'" key="home">
<Home />
</div>
<div $$if="view === 'about'" key="about">
<About />
</div>
</Motion>Define the named transition in your widget options:
export default {
transitions: {
slide() {
return {
keyframes: [
{ transform: ['translateX(100%)', 'translateX(0)'], duration: 300 },
{ transform: ['translateX(0)', 'translateX(-100%)'], duration: 300 }
]
}
}
}
}The in-template and in-handlers accessor for declared params. Read-only.
<p>Hello, {{ $params.name }}!</p>In the build function or Adapter API context, params arrive as part of the first argument:
build({ $params, slots, signals, events, attrs }) {
// ...
}In <script build>, use defineParams to declare and access typed params:
const $params = defineParams({ name: String, age: Number })$signals are declared events — explicitly defined as part of the widget's public API. Use them to communicate intentional output to the parent.
export default {
signals: ['updateCount', 'save'],
handlers: {
save() {
this.$signals.save(this.formData)
}
}
}$events are undeclared native DOM events that fall through to the widget's root element without explicit declaration.
$signalsvs$events: Use$signalsfor events your widget owns — things likesubmit,select,changethat are part of the widget's contract. Use$eventsfor native DOM events you're just passing through transparently.
Contains all attributes passed to a widget that are not declared as params or signals — including class, style, and any event listeners not explicitly declared.
<!-- Parent -->
<MyInput class="large" placeholder="Type here..." />
<!-- MyInput template -->
<input $$bind="$attrs" />By default, $attrs falls through to the widget's root element automatically. To disable:
{
buildConfig: {
forwardAttrs: false
}
}
// Adapter API:
// use Houxit.defineConfig() insteadThen manually apply $attrs wherever appropriate with $$bind="$attrs".
Slots let parent widgets inject content into a child widget's template.
<!-- Button widget template -->
<button class="btn">
<slot />
</button>
<!-- Usage -->
<Button>Click me</Button><!-- Modal widget template -->
<div class="modal">
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
</div>
<!-- Usage -->
<Modal>
<h2 #header>Confirm</h2>
<p>Are you sure?</p>
<button #footer @click="confirm">Yes</button>
</Modal>Scoped slots pass data up from the child widget to the parent's slot content. Use the context option method to expose data to the consumer — its return value becomes available in the slot:
export default {
context() {
return {
item: { label: 'houxit' },
index: 0
}
}
}<!-- Consume the exposed data with $$provide -->
<List $$provide="{ item, index }">
<strong>{{ index }}: {{ item.label }}</strong>
</List>Slot content can also receive data from a specific named <slot> through the context attribute:
<!-- Widget template -->
<slot name="default" context="{ item }" />
<!-- Parent consumes it -->
<MyList>
<span #default="{ item }">{{ item.name }}</span>
</MyList>Houxit ships a set of built-in widgets for common structural patterns.
The dynamic widget component — renders any widget passed to its self prop. Equivalent to Vue's <component :is="...">.
<Build *self="currentWidget" *name="userName" />self accepts a widget definition object, a function, a class, or a string name of a globally registered widget or HTML tag.
Wraps async subtrees and displays a fallback slot while they resolve.
<Suspense>
<Spinner #fallback />
<AsyncDashboard />
</Suspense>Works with {{@await}} blocks and widgets that have an async build function.
Renders children into a different DOM node — useful for modals, tooltips, and overlays that need to escape their parent's stacking context.
<Portal target="#modals">
<div class="modal-overlay">
<p>I'm rendered in #modals, not in-place.</p>
</div>
</Portal>Prevents re-renders of its children unless the specified key changes. Use for expensive or static subtrees.
<Memo *key="userId">
<ExpensiveProfile *id="userId" />
</Memo>Establishes a reactive value in the widget tree that descendant widgets can inject without prop-drilling. Works well with createAgent.
// Provide a value
import { inject } from 'houxit'<Provider name="theme" *value="currentTheme">
<App />
</Provider>// Inject in any descendant widget
const theme = inject('theme')Renders the current widget recursively. Use for tree views, nested menus, or any fractal/recursive structure.
<!-- TreeNode widget -->
<div class="node">
<span>{{ node.label }}</span>
<div $$if="node.children.length" class="children">
<Self $$for="child of node.children" *node="child" />
</div>
</div>Renders multiple root elements without a wrapping DOM node — useful when a widget needs to return more than one sibling element.
<Fragment>
<dt>Term</dt>
<dd>Definition</dd>
</Fragment>When templates aren't flexible enough — or when you need to construct UI programmatically — use the h function to write render functions.
import { h } from 'houxit'Signature:
h(type, props, ...children)type— a string tag name ('div','p'), a widget definition, or a built-in widget.props— an object of attributes, event handlers, and directives. Passnullif none.children— strings, numbers, or moreh()calls.
export default {
render() {
return h('ul', { class: 'list' },
...this.items.map(item =>
h('li', { key: item.id }, item.name)
)
)
}
}Render functions have the same expressive power as templates — reactive state reads are tracked, events can be attached, and all built-in widgets can be used:
import { h, Suspense, Fragment } from 'houxit'
render() {
return h(Fragment, null,
h('h1', null, 'Title'),
h(Suspense, {},
[
enSlot({ fallback: () => h('p', null, 'Loading...') }),
h(AsyncWidget, { id: this.id })
]
)
)
}Inside a .houxit file, use a dedicated <script render> block instead of <template> or the render option. It receives the widget context and returns a render expression.
<!-- UserCard.houxit -->
<script build>
import { useParams, h } from 'houxit'
useParams({
user: Object
})
</script>
<script render>
h('div', { class: 'card' },
h('img', { src: $params.user.avatar }),
h('h2', null, $params.user.name),
h('p', null, $params.user.bio)
)
</script>
<script render> takes precedence over <template> if both are present. It's the recommended approach when your widget's output is highly dynamic or computed.
Houxit ships with full TypeScript types. Params, signals, model properties, and build function arguments are all typed.
import { defineWidget, token } from 'houxit'
interface User {
name: string
email: string
}
export default defineWidget({
params: {
user: Object as () => User
},
build({ params }: { params: { user: User } }) {
const editedName = token(params.user.name)
return () => h('input', {
value: editedName.data,
'on:input': (e: Event) => {
editedName.data = (e.target as HTMLInputElement).value
}
})
}
})In <script build> blocks, use defineParams and defineSignals for typed declarations:
<script build lang="ts">
import { stream, defineParams, defineSignals } from 'houxit'
const $params = defineParams<{
label: string
disabled?: boolean
}>()
const emit = defineSignals<{
click: [MouseEvent]
}>()
function handleClick(e: MouseEvent) {
if (!$params.disabled) emit('click', e)
}
</script>
<template>
<button *disabled="$params.disabled" @click="handleClick">
{{ $params.label }}
</button>
</template>
Houxit.js is released under the MIT License.