Don't like q-component keywords? Rather call them a-block or something else?
Now you can use our script builder to customize the keywords for your qhtml instance. Click here to visit the script roller.
QHTML is a compact language and runtime for building web UIs with readable block syntax, reusable components, signals, and live QDOM editing.
- Live demo: https://qhtml.github.io/qhtml6/dist/demo.html
- Dev testbed: https://qhtml.github.io/qhtml6/dist/test.html
- Editor playground: https://qhtml.github.io/qhtml6/dist/editor.html
- Language wiki and more examples: https://www.datafault.net/packages/qhtml6/doc/
- Bumped the release line to
6.9.7. - Added QML-style
behavior on <property>withNumberAnimationas a property-write interceptor. - Added behavior-aware property writes through
QHtml.qSet()and bypassed animation-frame commits so animations do not recursively trigger themselves. - Expanded dimensional animation support so
px,%,vh, and other matching CSS units interpolate while intermediate values are mirrored as CSS-valid property strings. - Added
q-style-path-animationfor CSS motion-path animation sugar insideq-style. - Added
q-slot-defaultfor component slot fallback content and updated the UI component defaults that use it. - Added
q-ui-split-layoutand default slot content for the optionalq-ui-components.qhtmlcomponent set.
- Clone qhtml6 github repository
git clone https://github.com/qhtml/qhtml6.git- Copy qhtml6 into your project
qhtmlfolder
source ./deploy.sh /path/to/projectIn any .html file in your project directory just include qhtml.js to get started.
<script src="qhtml.js"></script>Optional: component library and UI tools (modal, form, grid, tabs, builder, editor):
Assuming your q-components.qhtml is located in the project folder /path/to/my-project/
<q-html>
q-import { q-components.qhtml }
</q-html>Files:
- Required:
qhtml.js - Recommended:
q-components.qhtml,w3.css - Optional:
q-components.qhtml,w3-tags.js,bs.css+bs-tags.js
<q-html>
h1 { text { Hello QHTML } }
p { text { Your first QHTML render is running. } }
</q-html>Resulting HTML:
<q-html>
<h1>Hello QHTML</h1>
<p>Your first QHTML render is running.</p>
</q-html><q-html>
div {
h2 { text { Product } }
p { text { Lightweight UI syntax. } }
}
</q-html>
Resulting HTML:
<q-html>
<div>
<h2>Product</h2>
<p>Lightweight UI syntax.</p>
</div>
</q-html><q-html>
div,section,h3 { text { Nested } }
</q-html>
Resulting HTML:
<q-html>
<div><section><h3>Nested</h3></section></div>
</q-html><q-html>
div#main.card {
p { text { Card body } }
}
</q-html>
Resulting HTML:
<q-html>
<div id="main" class="card">
<p>Card body</p>
</div>
</q-html>Multiple selectors with shorthand:
<q-html>
div#my-id.my-class,span.my-class,h2#id2 { hello world }
</q-html>
behavior on <property> intercepts writes before the final value is committed. It is not an on<Property>Changed handler. Animation frames commit with behavior bypass enabled, so an animation does not recursively start another animation.
q-component animated-panel {
q-property panelWidth: "50%"
behavior on panelWidth {
NumberAnimation {
duration: 1000
easing: "easeInOutQuad"
}
}
div.panel-fill {
style { width: 50%; min-height: 2rem; background: #1d4ed8; color: #ffffff; }
text { Animated panel }
}
onpanelWidthchanged(value) {
this.querySelector(".panel-fill").style.width = value;
}
onclick {
this.panelWidth = Math.random() * 100 + "%"
}
}
animated-panel { }
Inside a behavior, NumberAnimation defaults from to the current live property value and to to the intercepted requested value. Numeric dimensional values such as 100 normalize to 100px; matching units such as "50%" to "70%", "50vh" to "60vh", or "8rem" to "12rem" animate, while incompatible units fall back to an immediate commit with a warning. QHTML stores the unit sidecar in QDom property_extensions, interpolates the number, then commits either a plain number for unitless values or a CSS-valid string such as "123px" or "52%", so q-property setters and on<Property>Changed handlers can project those values into CSS directly.
This lets a component keep animation state in a q-property while projecting CSS-ready values in the property changed handler. During the animation above, panelWidth receives intermediate values like "43.2%"; the handler writes those values directly to .panel-fill.style.width, and the final frame commits the exact requested percent string.
Use q-bind-css inside a component when the property can be projected directly to a writable CSS reference:
q-component animated-box {
q-property w: "200px"
q-bind-css { this.component.w this.component.style.width }
behavior on w {
NumberAnimation { duration: 250 }
}
}
The first q-bind-css expression must reference a q-property declared on the same component. The second expression must resolve to a writable target such as this.component.style.width.
Resulting HTML:
<q-html>
<div id="my-id" class="my-class">
<span class="my-class">
<h2 id="id2">hello world</h2>
</span>
</div>
</q-html>Component instances also support selector shorthand:
q-component my-card { div { text { Card } } }
my-card#card-1.primary { }
<q-html>
a {
href: "https://example.com"
target: "_blank"
text { Open Example }
}
</q-html>
Resulting HTML:
<q-html>
<a href="https://example.com" target="_blank">Open Example</a>
</q-html><q-html>
p {
style { font-size: 20px; margin: 0; }
text { Plain text content }
}
div { html { <strong>Real HTML fragment</strong> } }
</q-html>
Resulting HTML:
<q-html>
<p style="font-size: 20px; margin: 0;">Plain text content</p>
<div><strong>Real HTML fragment</strong></div>
</q-html>q-component defines a runtime host element with:
q-propertyfieldsfunction ... {}methods onthis.componentq-signal ...signals onthis.componentq-alias ... { ... }computed alias properties onthis.componentslot { name }placeholders for projection
<q-html>
q-component app-card {
q-property { title }
div {
h3 { text { ${this.component.title} } }
slot { body }
}
}
app-card {
title: "Welcome"
body { p { text { Projected content. } } }
}
</q-html>
Use q-slot-default inside a component definition to provide fallback QHTML for a slot when the instance does not supply that slot. An explicit empty slot suppresses the default.
q-component notice-card {
q-slot-default body {
p { text { Default notice text. } }
}
article {
slot { body }
}
}
notice-card { }
notice-card {
body { p { text { Custom notice text. } } }
}
notice-card {
body { }
}
Use this form when you want an in-scope reference handle:
q-component q-cart {
q-property total: "0.00"
}
q-cart myCart { total: "39.99" }
button {
text { Print total }
onclick {
console.log(myCart.total); // "39.99"
}
}
This is the canonical instance form:
- Definition:
q-component <type> { ... } - Instantiation with handle:
<type> <name> { ... }
Dot walking lets you dereference named instances and their fields directly in declarative expressions.
q-component catalog-store {
q-property title: "Main Catalog"
q-property currency: "USD"
}
catalog-store store1 { }
q-component product-card {
q-property label: store1.title
q-property unit: store1.currency
}
product-card card1 {
div { text { ${card1.label} (${card1.unit}) } }
}
Valid usage patterns:
<instanceName>.<property><instanceName>.<property>.<nestedProperty>- chained use inside
${...}and property defaults
Inside a q-component definition, descendants can also reference the enclosing component by type name:
q-component mycomp1 {
q-property myprop: "hello"
mycomp2 {
onReady { console.log(mycomp1.myprop) } // "hello"
}
}
When nested owners share the same type, repeating the type token walks upward:
// inside nested mycomp1 scope:
// mycomp1 -> nearest mycomp1 owner
// mycomp1.mycomp1 -> next enclosing mycomp1 owner
Use dot walking for declarative wiring between named instances instead of selector-based lookup code.
Named instances resolve by scope/context, not by global selector lookup. A reference is valid only where that name is in scope.
q-component comp-a { q-property value: "hello" }
comp-a rootA { }
q-component comp-b {
// valid: rootA is in an outer/available scope
q-property fromRoot: rootA.value
}
comp-b rootB { }
q-component parent-scope {
comp-a nestedA { value: "inside-parent" }
}
parent-scope p1 { }
Practical rule:
- Prefer direct named-instance references inside the same valid scope.
- Use selectors only for generic DOM traversal tasks, not for routine component-to-component wiring.
q-component my-comp {
q-property title
q-property selected: true
}
Declared properties can reference another in-scope property path directly:
q-component comp1 { q-property prop1: "something" }
comp1 mycomp1 { prop1: "testing 1 2 3" }
q-component comp2 extends comp1 {
q-property prop2: mycomp1.prop1
}
comp2 mycomp2 { prop1: "testing" }
Behavior:
mycomp2.prop1resolves to"testing".mycomp2.prop2resolves to"testing 1 2 3".- Direct reads and inline interpolation resolve consistently:
console.log(mycomp2.prop2)${mycomp2.prop2}
Use this for simple declarative value wiring between named instances without extra query/select boilerplate.
Each declared q-property automatically exposes a changed signal handler name in the form on<Property>Changed.
q-component counter-box {
q-property count: 0
onCountChanged {
console.log("new count =", event.detail.params.value);
}
function increment() {
this.count = Number(this.count) + 1;
}
}
Notes:
onCountChangedfires only whencountchanges to a different value.- Payload is available at
event.detail.params.valueandevent.detail.args[0]. - Each declared property also gets an implicit runtime signal function (
countChanged,titleChanged, etc), so you can use.connect(...)in addition toon<Property>Changed. on<Property>Changedmatching is case-insensitive (oncountchanged,onCountChanged,onCoUnTcHaNgEdall work).
You can assign typed array and map values directly to component properties. q-array becomes a JavaScript array and q-map becomes a plain JavaScript object on the mounted component instance.
q-component my-comp {
q-property mydivs: q-array { "hello world", 5, q-array { "multi-dimensional", 4 } }
q-property settings: q-map {
title: "Example"
count: 2
flags: q-array { true, false }
nested: q-map { enabled: true }
}
}
my-comp { }
At runtime:
document.querySelector("my-comp").mydivs[0]returns"hello world"document.querySelector("my-comp").mydivs[2]returns["multi-dimensional", 4]document.querySelector("my-comp").settings.nested.enabledreturnstrue
Named q-array and q-map declarations still work, and you can assign them to declared component properties by name.
q-array shared-items { "hello world", 5, q-array { "multi-dimensional", 4 } }
q-map shared-settings {
title: "Shared config"
count: 2
nested: q-map { enabled: true }
}
q-component my-comp {
q-property mydivs: shared-items
q-property settings: shared-settings
}
my-comp { }
q-component my-comp {
q-property mydivs: q-array { "hello world", 5, q-array { "multi-dimensional", 4 } }
q-property settings: q-map { title: "Example", nested: q-map { enabled: true } }
}
my-comp { }
button {
text { Show values }
onclick {
const comp = document.querySelector("my-comp");
alert(comp.mydivs[0]);
console.log(comp.mydivs[2]);
console.log(comp.settings.nested.enabled);
}
}
property <name>: ... also works as shorthand inside q-component, while property <name> { ... } still means "bind this child node to a component property".
q-model normalizes model data and exposes a consistent runtime API (count(), at(), values(), add(), insert(), remove(), subscribe()) regardless of source shape.
q-model my-model { q-array { 5, 10 } }
onReady {
var model = this["my-model"];
model.add(15);
console.log(model.count()); // 3
console.log(model.at(1)); // 10
console.log(model.values()); // [5, 10, 15]
}
q-model-view renders its child template once per model entry using the alias defined by as { ... }.
q-array my-source { 5, 10, q-map { name: "tom" } }
q-model-view {
q-model { my-source }
as { item }
div { text { ${item && item.name ? item.name : item} } }
}
Use for when you want inline repeated template expansion without creating a q-model-view node:
q-component list-demo {
q-property items: q-array { "one", "two", "three" }
ul {
for (item in this.component.items) {
li { text { ${item} } }
}
}
}
Accepted source inputs:
- q-array and JS arrays (for example
this.component.items) - q-map / plain object (iterates keys)
q-modelhelpers such as.values()/.keys()- function return values that evaluate to arrays/objects (for example
this.component.getItems()) - primitive values (treated as single-entry iteration)
Notes:
forexpression scope follows runtime inline expression rules.- For component state, prefer explicit component references (
this.component.<name>).
q-timer is a named top-level construct that declares a runtime timer directly:
q-timer myTimer {
interval: 3000
repeat: true
running: true
onTimeout {
alert("hello world");
}
}
Behavior:
repeat: trueuses nativesetInterval(...).repeat: falseuses nativesetTimeout(...).- The named timer handle is exported globally as
window.<name>(for examplewindow.myTimer). - The same named handle is also exposed to inline expression scope (for example
${myTimer}in runtime-evaluated expressions). - The named timer handle exposes a
timeoutsignal object with.connect(...),.disconnect(...), and.emit(...).
Example (q-connect with timer timeout):
q-timer tickTimer {
interval: 120
repeat: true
running: true
}
q-component receiver {
q-property count: 0
function step() { this.component.count = Number(this.component.count) + 1; }
}
receiver rx { }
q-connect { tickTimer.timeout rx.step }
Name collisions / overwrite behavior:
- If multiple
q-timerdeclarations use the same name in the same mounted host, the last declaration wins for the exported name (window.<name>points to the last timer handle). - Earlier same-name timers may still be running if they were already started; only the exported reference is overwritten.
- On host re-render/unmount, runtime-managed keyword timers for that host are cleared and re-created from current declarations.
Recommendation:
- Use unique timer names per host to avoid handle collisions.
q-var declares a named runtime value in the current QHTML scope. The block is evaluated as JavaScript and is intended for stored strings, numbers, objects, arrays, and functions. Use q-property when you need component property binding; use q-var when you need an in-scope runtime value or helper.
q-var names { ["ada", "grace", "katherine"] }
q-var settings { ({ tone: "calm", count: names.length }) }
q-var makeLabel { function(name) { return settings.tone + ": " + name; } }
ul {
li { text { ${makeLabel(names[0])} } }
li { text { total=${settings.count} } }
}
Every q-var handle exposes:
.value: the current value..get(): returns the current value..set(value): updates the stored value.
q-var count { 0 }
button {
text { increment }
onclick { count.set(count.value + 1); }
}
button {
text { replace with object }
onclick { count.value = { clicked: true, total: count.value }; }
}
Functions stored in q-var can be called directly:
q-var formatPrice { function(value) { return "$" + Number(value).toFixed(2); } }
p { text { ${formatPrice(14.5)} } }
q-var follows QContext rules. A q-var declared in a host or anonymous container is visible to siblings and descendants. A q-var declared inside a named component is owned by that component and can be reached by descendants directly or by outside callers through the component reference.
q-var hostMessage { "available to this host" }
q-component note-card {
q-var localMessage { "inside " + this.component.id }
p { text { ${hostMessage} / ${localMessage} } }
}
note-card firstCard#first { }
button {
text { read component q-var }
onclick { console.log(firstCard.localMessage); }
}
q-var can also feed dynamic QHTML fragments. When a q-var contains a partial fragment head, qhtml(varName) { ... } appends the block body and closes missing braces:
q-var cardHead { "div.card,section.body {" }
qhtml(cardHead) {
h3 { text { Runtime fragment } }
p { text { This body completes the fragment stored in cardHead. } }
}
q-switch declares a named runtime function that maps primitive keys to JavaScript expression results. The shorthand switch parses the same way.
q-switch labelFor {
15: { "hello world" }
"test": { 32 }
false: { "false is preserved" }
*: { "fallback" }
}
div { text { ${labelFor(15)} } }
button {
text { run switch }
onclick {
console.log(labelFor("test"));
}
}
Switch functions are normal in-scope named values, so component code and event handlers can call them directly. Falsey case results such as 0, false, and "" are returned as real matches; the * case is used only when no key matches.
q-switch can also feed dynamic QHTML:
q-switch bodyFor {
"card": { "div.card { text { hello world } }" }
*: { "" }
}
qhtml(bodyFor("card"))
q-canvas declares a named canvas element and exports that handle by name:
q-canvas myCanvas {
width: 320
height: 180
}
You can draw manually through the exported context helper:
onReady {
myCanvas.context.clearRect(0, 0, 320, 180);
myCanvas.context.fillStyle = "rgba(16,185,129,0.9)";
myCanvas.context.fillRect(20, 20, 120, 80);
}
Notes:
q-canvas <name>exports the canvas handle aswindow.<name>and host-scoped<name>.<name>.contextpoints to the2drendering context for that specific canvas.- Canvas rendering can be timer-driven (
q-timer) or signal-driven depending on your component flow.
particle-emitter is a native custom element registered by qhtml.js. q-particle-emitter is an alias for the same emitter system. It is not a q-component and does not require q-components.qhtml. Place it inside any positioned container, configure it with attributes, and control it with the boolean running property or the start(), stop(), clear(), and burst(num, x, y) methods.
div#energy-field {
style {
position: relative;
width: 420px;
height: 220px;
overflow: hidden;
background: #07111f;
}
particle-emitter#energy-emitter {
emitRate: "84"
lifetime: "3600"
lifetimeVariation: "900"
x: "210"
y: "214"
xVariation: "145"
yVariation: "8"
xVelocity: "0.35"
yVelocity: "-1.25"
xVelocityVariation: "0.45"
yVelocityVariation: "0.4"
xAcceleration: "0.015"
yAcceleration: "-0.018"
startSize: "10"
endSize: "30"
startOpacity: "0.35"
endOpacity: "0.02"
maxActiveParticles: "96"
running: "false"
interval: "18"
src: "/dist/assets/particle.png"
emitterMask: "/dist/assets/particle-mask-star.svg"
}
}
button { text { Start } onclick { document.querySelector("#energy-emitter").running = true; } }
button { text { Stop } onclick { document.querySelector("#energy-emitter").running = false; } }
button { text { Burst } onclick { document.querySelector("#energy-emitter").burst(24, 210, 120); } }
Useful attributes:
emitRate: particles created per second whilerunningis true.running: boolean emission switch; assigningemitter.running = true/falseupdates therunningattribute.lifetime/lifetimeVariation: how long each particle lives, in milliseconds.x,y,xVariation,yVariation: spawn origin and randomized spawn spread inside the emitter container.xVelocity,yVelocity,xAcceleration,yAcceleration: per-frame movement controls.startSize,endSize,startOpacity,endOpacity: interpolation values over each particle lifetime.maxActiveParticles: maximum simultaneous particles.maxParticlesorstopAfter: optional total particle limit before auto-stopping.src,mask,color,colorOpacity/color-opacity: sprite source, optional per-particle alpha mask, and fallback/tint color. Whensrcis present, the source image remains visible;coloris applied as a transparent overlay or masked backdrop instead of replacing the image pixels. SetcolorOpacity/color-opacityto0to disable the color effect.emitterMask/emitter-mask: optional emitter-area alpha mask. Particles whose centers fall outside non-transparent mask pixels are skipped during rendering.
Useful methods:
start()/stop(): toggles continuous emission.clear(): removes current particles and pending bursts.burst(num, x, y): queuesnumparticles emitted at the currentemitRatefrom the supplied origin. The emitter's configuredx/yattributes are not changed.
See doc/11-particle-emitter/ for the full attribute reference.
q-spritesheet renders one frame of a larger spritesheet image inside a fixed viewport using CSS background-position math.
q-spritesheet demoSheet {
source: "assets/test-ss.png"
frameCount: 18
frameStart: 0
frameStop: 17
frameDuration: 90
frameWidth: 0
frameHeight: 0
running: true
repeat: true
interpolate: false
}
Notes:
- Public API uses camelCase (
frameCount,frameStart,frameStop,frameDuration,frameWidth,frameHeight,currentFrame). - If
frameWidthis missing/invalid, runtime derivesframeWidth = imageWidth / frameCount. q-spritesheetincludes an internalq-timerand advances frames via declarative wiring:q-connect { frameTimer.timeout this.component.nextFrame }.- Control methods:
start(),stop(),nextFrame().
q-worker can be declared inside a q-component to run worker methods off the main thread.
q-component worker-demo {
q-property output: "waiting"
q-worker cruncher {
q-property nums: q-array { 1, 2, 3, 4 }
q-signal finished(total)
function sumAll() {
var total = 0;
for (var i = 0; i < this.nums.length; i += 1) {
total = total + Number(this.nums[i] || 0);
}
this.finished(total);
return total;
}
}
onFinished {
this.component.output = String(event.detail.params.total);
}
onReady {
this.component.cruncher.sumAll().then(function(total) {
this.component.output = String(total);
}.bind(this));
}
}
Notes:
- Worker methods return Promises.
- Worker
q-propertyandq-signaldeclarations are supported in worker scope. - Signals emitted from worker methods re-enter normal component signal handling (
on<Signal>/.connect(...)).
q-tree-view consumes model data from the same q-model pipeline and renders nested branches/leaves with native details/summary. For the current end-to-end example, see dist/demo.html.
Use extends when you want one component to inherit reusable behavior from another component instead of wrapping one component inside another.
Inherited component parts include:
q-propertyfunction ... { }q-signalq-aliasonReadyand other lifecycle hooksslot { ... }placeholders- template children / rendered markup
Child components are merged after parent components, so child methods and declarations with the same name win.
q-component counter-base {
q-property count: 0
function increment() {
this.count = Number(this.count) + 1;
this.update(this.qdom().UUID);
}
}
q-component counter-button extends counter-base {
button {
type: "button"
onclick { this.component.increment(); }
text { Count: ${this.component.count} }
}
}
counter-button { }
Use single inheritance when one parent component already represents the exact reusable base behavior you want.
q-component counter-base {
q-property count: 0
function increment() {
this.count = Number(this.count) + 1;
this.update(this.qdom().UUID);
}
}
q-component hello-base {
function hello() { alert("hello world"); }
}
q-component counter-toolbar extends counter-base extends hello-base {
button {
type: "button"
onclick { this.component.increment(); }
text { Count: ${this.component.count} }
}
button {
type: "button"
onclick { this.component.hello(); }
text { Say Hello }
}
}
counter-toolbar { }
Use multiple inheritance when you want to compose a new component from several reusable behavior blocks without adding extra wrapper components just to pass features through.
q-component base-a {
function label() { return "A"; }
}
q-component base-b {
function label() { return "B"; }
}
q-component final-comp extends base-a extends base-b {
function label() { return "child"; }
}
Merge order is left-to-right, then child last:
base-abase-bfinal-comp
So in the example above, final-comp.label() returns child. If the child did not define label(), then base-b.label() would win over base-a.label().
q-wasm is supported inside q-component and exposes this.component.wasm with:
ready(Promise)call(exportName, payload)terminate()
q-component wasm-card {
q-signal computed(result)
q-wasm {
src: "/wasm/demo.wasm"
mode: worker
awaitWasm: true
timeoutMs: 5000
maxPayloadBytes: 65536
exports { init compute }
bind {
compute -> method runCompute
compute -> signal computed
}
}
onReady {
this.component.wasm.ready.then(() => this.component.runCompute({ value: 7 }));
}
}
Notes:
q-wasmis valid only insideq-component.mode: workeris default; if worker mode cannot be used, runtime falls back to main thread.allowImports { ... }is supported in main-thread mode.
This creates a real child node and assigns it to this.component.<name>.
q-component my-comp {
property builder { q-builder { } }
onReady { this.component.builder.setAttribute("data-bound", "1"); }
}
q-component mycomp {
q-alias myotherprop { return document.querySelector("#mydiv").myprop; }
}
q-component mytarget {
q-property myprop: "hello world"
}
mytarget { id: "mydiv" }
mycomp {
div { text { ${this.component.myotherprop} } }
}
q-macro is source expansion, not a rendered node.
It behaves like a reusable inline source generator.
q-macro badge {
slot { label }
return {
span.badge { text { ${this.slot("label")} } }
}
}
div {
badge { label { hello world } }
}
Inside macro output, ${name} resolves using the current scoped references (macro slots).
q-macro scoped-label {
slot { value }
return {
p { text { value=${this.slot("value")} } }
}
}
scoped-label {
value { demo-ref }
}
Use slot { name } for raw slot insertion blocks, and ${name} for inline placeholder insertion.
<q-html>
button {
text { Click }
onclick { this.textContent = "Clicked"; }
}
</q-html>
Resulting HTML:
<q-html>
<button onclick="this.textContent = "Clicked";">Click</button>
</q-html>onReady {} runs after the host’s content is mounted.
<q-html>
onReady { this.setAttribute("data-ready", "1"); }
div { text { Host ready hook executed. } }
</q-html>
You can chain additional script blocks after any on* handler:
q-component mycomp {
onready {
this.component.status = "ready";
} then {
this.component.status = this.component.status + "-next";
} then {
console.log(this.component.status);
}
}
The same pattern works for signal/property-changed handlers:
q-component mycomp {
q-signal ping(v)
q-property out: ""
onping(v) {
this.component.out = String(v);
} then {
this.component.out = this.component.out + "-then";
}
}
Behavior:
- blocks run in order.
- if a block returns a Promise, the next block waits for it.
- if a block throws/rejects, QHTML logs the error and continues the chain.
- no implicit previous-result variable is injected; use component/property/event state explicitly.
Resulting HTML:
<q-html data-ready="1">
<div>Host ready hook executed.</div>
</q-html>Use $("<selector>") inside QHTML runtime JavaScript to query within the current <q-html> root only.
$("#sender")is equivalent tothis.component.qhtmlRoot().querySelector("#sender")(or closest<q-html>root).- For global page lookup, use
document.querySelector(...).
q-component notifier {
function notify(msg) { this.setAttribute("data-msg", msg); }
onReady { $("#sender").sendSignal.connect(this.component.notify) }
}
q-component sender {
q-signal sendSignal(message)
}
q-style + q-theme are the preferred styling model for new code.
q-style-class lets a style definition add CSS classes and inline properties together.
q-style panel-style {
q-style-class { w3-container w3-round-large }
backgroundColor: #e0f2fe
color: #0c4a6e
}
Apply it directly:
panel-style,div { text { Styled panel } }
Or through a theme rule:
q-theme app-theme {
.panel { panel-style }
}
Notes:
q-style-classmerges class names into the elementclassattribute.- Inline
q-styledeclarations are still applied viastyle="". - If both class CSS and inline declarations target the same property, inline wins.
q-transition defines a named CSS transition recipe, and q-style-transition attaches one or more transition recipes to a style.
q-transition fade-in {
property { opacity }
delay { 50 }
timing { ease-in-out }
duration { 3000 }
}
q-style panel-style {
q-style-transition { fade-in }
}
q-theme app-theme {
.panel { panel-style }
}
Notes:
timingaccepts CSS timing-function values directly (for exampleease,ease-in-out,cubic-bezier(...)).- Numeric
duration/delayvalues are interpreted as milliseconds (3000->3000ms). - Multiple transition references can be listed in
q-style-transitionand are combined into a comma-separated CSStransitionvalue.
Example with multiple q-transitions in one style:
q-transition fade {
property { opacity }
duration { 250 }
timing { ease-out }
}
q-transition slide {
property { transform }
duration { 450 }
delay { 50 }
timing { cubic-bezier(0.2, 0.8, 0.2, 1) }
}
q-style card-anim {
q-style-transition { fade slide }
}
q-theme cards-theme {
.card { card-anim }
}
Example with direct style invocation:
q-transition quick-fade {
property { opacity }
duration { 180 }
timing { ease-in }
}
q-style quick-fade-style {
q-style-transition { quick-fade }
}
quick-fade-style,div#boxA { text { transition-ready } }
q-style-path-animation is q-style sugar for CSS motion-path animation. Inline-compatible declarations are applied to the element, and QHTML injects a generated @keyframes style tag for the unsupported inline part.
q-style mover {
width: 40px;
height: 40px;
background: red;
q-style-path-animation {
path: "M 50 50 C 400 50, 50 300, 400 300"
duration: 1200
easing: "ease-in-out"
anchorPoint: "center"
rotation: "auto"
repeat: "true"
}
}
q-theme motion-theme {
.box { mover }
}
This emits offset-path, offset-distance, offset-anchor, offset-rotate, and animation on matching elements, plus a generated @keyframes rule that moves offset-distance from 0% to 100%.
q-anchor positions an element or component host relative to another rendered element. The legacy block form and directional shorthand both map to the same anchor rules.
q-component label-box {
div { text { ${this.component.label} } }
}
label-box sourceBox { label: "Source" }
div {
q-anchor-left { sourceBox.right }
q-anchor-top { sourceBox.bottom }
text { Anchored below the source box }
}
Legacy block form remains supported:
div {
q-anchor {
left: sourceBox.right;
top: sourceBox.bottom
}
}
Supported shorthands are q-anchor-left, q-anchor-right, q-anchor-top, q-anchor-bottom, q-anchor-center, q-anchor-hcenter, and q-anchor-vcenter. Named side references support .left, .right, .top, .bottom, .center, .hcenter, and .vcenter; non-side expressions are evaluated as CSS values.
q-painter defines a named paint worklet body using declarative q-property defaults plus an onpaint { ... } block.
q-painter panel-painter {
q-property someprop: "rgba(40,80,160,0.9)"
onpaint {
this.fillStyle = this.someprop
this.fillRect(0, 0, this.width, this.height)
}
}
q-style panel-style {
width: 84px
height: 26px
q-style-painter {
background { panel-painter }
}
}
q-style-painter currently supports semantic slots:
background { painterName }->background-image: paint(...)border { painterName }->border-image-source: paint(...)mask { painterName }->mask-image/-webkit-mask-image
Notes:
- Painter names are resolved by QHTML scope/context, then registered with internally unique worklet names.
q-propertyentries inq-painterare exposed toonpaintasthis.<property>.onpaintdoes not take parameters; usethis.width,this.height, and painter properties.- If
CSS.paintWorkletis unavailable, QHTML logs a warning and skips painter attachment.
q-style panel {
backgroundColor: #eff6ff
color: #1e293b
border: 1px solid #93c5fd
}
panel,div { text { Styled panel } }
q-style card-shell {
q-style-class { w3-card w3-round-large w3-padding }
borderColor: #cbd5e1
}
q-style title-accent { color: #1d4ed8 }
q-style body-muted { backgroundColor: #64748b }
q-theme article-theme {
h3 { title-accent body-muted }
p { body-muted }
}
Theme rules can also include anonymous q-style { ... } blocks when the style is only used by that selector:
q-style body-panel { backgroundColor: #e2e8f0 }
q-theme article-theme {
h3 { q-style { color: #1d4ed8 } }
.summary { q-style { color: #334155 } body-panel }
}
q-default-theme is a fallback theme. It applies first, and any conflicting q-theme rules in scope replace it.
q-style panel-base { backgroundColor: #eef3fb color: #0f172a }
q-style panel-override { backgroundColor: #ffedd5 color: #7c2d12 }
q-default-theme card-theme {
.card { panel-base }
}
q-theme card-demo-theme {
card-theme { }
.card { panel-override }
}
article-theme {
div {
h3 { text { Title } }
p { text { Description } }
}
}
q-theme base-theme {
button { button-base }
}
q-theme admin-theme {
base-theme { }
.danger { button-danger }
}
q-style button-base {
q-style-class { w3-button w3-round }
backgroundColor: #0f766e
color: #ffffff
}
Notes:
q-style-classmerges into the elementclassattribute.- Inline declarations from
q-styleare written tostyle=""and win on property conflicts. - Themes can be declared once and reused as lightweight styling scopes.
q-bind is deprecated and treated the same as q-script.
Use q-script for assignment expressions.
Use assignment-form q-script with q-property.
q-component my-component {
q-property myprop: q-script { return "bound-" + (2 + 3) }
div { text { ${this.component.myprop} } }
}
my-component { }
q-script {} runs JavaScript and replaces itself with the returned value:
- If the return looks like QHTML, it is parsed as QHTML.
- Otherwise, it becomes a text node.
<q-html>
div {
q-script { return "p { text { Inserted by q-script } }"; }
}
</q-html>
<q-html>
div {
data-note: q-script { return "n:" + (4 + 1) }
text { q-script { return "script-inline"; } }
}
</q-html>
${expression} is inline expression syntax for string content.
- It resolves when the final HTML string value is rendered.
- It is not a watcher by itself.
- Re-evaluation is explicit (for example, manual setter calls and explicit update flows).
<q-html>
div {
title: "Current user: ${window.currentUser}"
text { Hello ${window.currentUser} }
}
</q-html>
q-component user-card {
q-property name: "Guest"
h3 { text { ${this.component.name} } }
}
q-macro badge {
slot { label }
return { span { text { ${label} } } }
}
q-component ${dynamicName} { } // invalid
q-keyword ${alias} { q-component } // invalid
${tagName} { text { hi } } // invalid
q-component counter-label {
q-property label: q-bind { return "Count: " + window.count; } // same as q-script (deprecated alias)
div { text { ${this.component.label} } }
}
q-keyword remaps a keyword head inside the current scope.
q-keyword component { q-component }
component card-box {
div { text { hello } }
}
card-box { }
Scope is local to the parent block and inherited by children:
div {
q-keyword box { span }
box { text { inside } } // -> span { ... }
}
box { text { outside } } // unchanged (no alias in this scope)
Invalid direct aliasing is rejected:
q-keyword a { q-component }
q-keyword b { a } // error: alias cannot target another alias
-
Note* it is still may be possible to design a system that loops forever using q-keword combined with other features.
-
If you want to create that and freeze your web browser, there are only basic safe guards in place that do not recursively prevent such behavior on all cases.
-
Note * If you define your keyword as
#or.or something like that, there may be some undesired artifacts rendered into the HTML DOM output.
There are two signal forms: component signals (function-style) and QHTML signal definitions.
<q-html>
q-component sender-box {
q-signal sent(message)
button {
text { Send }
onclick { this.component.sent("Hello"); }
}
}
q-component receiver-box {
function onMessage(message) { this.querySelector("#out").textContent = message; }
sender-box { id: "sender" }
p { id: "out" text { Waiting... } }
onReady { this.querySelector("#sender").sent.connect(this.component.onMessage); }
}
receiver-box { }
</q-html>
Defining q-signal menuItemClicked { ... } lets you “call” it by writing menuItemClicked { ... }.
This dispatches a DOM CustomEvent named menuItemClicked with event.detail.slots and event.detail.slotQDom.
<q-html>
q-signal menuItemClicked {
slot { itemId }
}
div {
menuItemClicked { itemId { A } }
p { text { signal-syntax-ok } }
}
</q-html>
<q-html>
q-component sender-box {
q-signal sent(message)
function sendNow(message) {
this.sent(message); // direct emit
}
}
q-component receiver-box {
q-property value: "waiting"
function onMessage(message) {
this.component.value = message;
}
div#out { text { ${this.component.value} } }
}
sender-box sender1 { id: "sender1" }
receiver-box receiver1 { id: "receiver1" }
onReady {
// imperative connect
sender1.sent.connect(receiver1.onMessage);
}
button {
text { Send via imperative connect }
onclick { sender1.sendNow("hello-from-connect"); }
}
</q-html>
<q-html>
q-component sender-box {
q-signal sent(message)
function sendNow(message) {
this.sent(message);
}
}
q-component receiver-box {
q-property value: "waiting"
function onMessage(message) {
this.component.value = message;
}
div#out { text { ${this.component.value} } }
}
sender-box senderA { id: "senderA" }
receiver-box receiverA { id: "receiverA" }
// declarative connect sugar
q-connect { senderA.sent receiverA.onMessage }
button {
text { Send via q-connect wiring }
onclick { senderA.sendNow("hello-from-q-connect"); }
}
</q-html>
Use this rule of thumb:
- Use direct emit (
this.sent(...)) inside the sender component. - Use
.connect(...)when wiring at runtime in JS lifecycle/event logic. - Use
q-connect { ... }for declarative-only wiring.
q-state-machine is a component-backed state switcher. Use it when one named runtime host should render one of several declarative QHTML bodies based on a state property.
You can think of it as a compact version of a component with a main render slot plus an onstatechanged handler that swaps that slot. The difference is that q-state-machine keeps the state blocks declarative and lets the framework do the active-state QDOM swap.
<q-html>
q-state-machine mymachine {
state1 {
div,span,h3 { text { hello world } }
}
state2 {
div,span,h4 { text { activated state2 } }
}
}
button {
text { show state1 }
onclick { mymachine.state = "state1"; }
}
button {
text { show state2 }
onclick { mymachine.state = "state2"; }
}
</q-html>
Behavior:
mymachineis a named component instance.mymachine.stateis a declared q-property.- The first state is the initial state unless you set
state: "stateName"in the machine body. - Assigning
mymachine.staterenders only that state's QHTML inside the<q-state-machine>host.
Every state machine has a normal component signal named statechanged. It emits the new state as the first argument.
<q-html>
q-component status-panel {
q-property current: "waiting"
function setStateName(value) {
this.component.current = String(value);
this.querySelector("#out").textContent = "state=" + this.component.current;
}
div#out { text { state=waiting } }
}
q-state-machine mymachine {
state1 { p { text { State one } } }
state2 { p { text { State two } } }
}
status-panel panel { }
q-connect { mymachine.statechanged panel.setStateName }
button {
text { activate state2 }
onclick { mymachine.state = "state2"; }
}
</q-html>
Equivalent imperative wiring also works:
onReady {
mymachine.statechanged.connect(panel.setStateName);
}
Because a state machine is component-backed, the machine body can include component-level declarations before the state blocks:
<q-html>
q-state-machine mymachine {
q-property note: "created"
q-signal saved(value)
function mark(value) {
this.component.note = String(value);
this.component.saved(this.component.note);
}
state1 {
div { text { Editing draft } }
}
state2 {
div { text { Published } }
}
}
button {
text { publish }
onclick {
mymachine.mark("published");
mymachine.state = "state2";
}
}
</q-html>
Use these declarations the same way you use component declarations:
q-propertystores machine-level values.functionadds methods to the<q-state-machine>host.q-signalcreates normal connectable signal functions.- State blocks stay declarative and contain the QHTML rendered for each state.
State bodies can instantiate components. Those named instances are scoped inside the active machine host, so the stable path is through the machine instance:
q-component message-card {
q-property label: ""
div { text { ${this.component.label} } }
}
q-state-machine mymachine {
state1 {
message-card card { label: "value from state1" }
}
state2 {
message-card card { label: "value from state2" }
}
}
button {
text { read active card }
onclick {
console.log(mymachine.card.label);
}
}
When the state changes, the active state's QHTML is re-rendered and the active child aliases are recreated for that machine context.
q-rewrite is a pre-parse macro that expands calls like name { ... } before the rest of QHTML is parsed.
q-rewrite pill {
slot { label }
span { class: "pill" slot { label } }
}
pill { label { text { OK } } }
q-rewrite choose-class {
slot { active }
return {
q-script {
return this.qdom().slot("active").trim() === "true" ? "on" : "off";
}
}
}
div { class: choose-class { active { true } } }
Mounted <q-html> elements expose .qdom() (the source-of-truth tree). Mutate QDOM, then call .update() to re-render.
Any HTMLElement inside a mounted tree can also call .qdom(), which resolves using the closest q-component host when present, then the nearest <q-html> host.
When q-keyword aliases are active during parse, generated QDOM nodes include a keywords map (effective alias table at parse time).
<q-html id="page">
ul { id: "items" }
</q-html>
<script>
const host = document.querySelector("#page");
const root = host.qdom();
const list = root.find("#items");
list.appendNode(list.createQElement("li", { textContent: "Added via qdom()" }));
host.update();
</script>const host = document.querySelector("q-html");
host.qdom().find("#items").replaceWithQHTML("ul { li { text { Replaced } } }");
host.update();document.querySelector("q-html").qdom().find("#items").rewrite(function () {
return "div { class: 'box' text { Rewritten } }";
});
document.querySelector("q-html").update();const host = document.querySelector("q-html");
const serialized = host.qdom().serialize();
host.qdom().deserialize(serialized, false); // append
host.update();
host.qdom().deserialize(serialized, true); // replace
host.update();const comp = document.querySelector("my-component");
const qnode = comp.qdom(); // component instance qdom
const props = qnode.properties(); // shallow copy of current props
const val = qnode.getProperty("title"); // single prop lookup
qnode.property("title", "New title"); // set via helper
comp.update(); // re-render this component scopethis.component.update(); // this component subtree
this.component.root().update(); // whole <q-html>dist/demo.htmlis the component usage gallery.q-editorsupports authoring live QHTML and previewing output.q-builderprovides visual inspect/edit workflows on mounted<q-html>content.
<q-editor> takes QHTML as literal text content (not nested <q-html>).
<q-editor>
h3 { text { Hello from q-editor } }
</q-editor>q-import records import metadata in the host QDOM (meta.imports / meta.importCacheRefs) and resolves definitions from runtime imports. Imported component/template/signal definitions become available without inlining full imported source into the host QDOM.
q-import is nocache by default (always fetch from network/source).
<q-html>
q-import { q-components/q-modal.qhtml }
q-modal { title { text { Hello } } body { text { Modal body } } }
</q-html>
Use cache to enable localStorage-backed import caching (qhtml.import.records + qhtml.import.index) for that import:
<q-html>
q-import { q-components/q-modal.qhtml cache }
q-modal { title { text { Cached import } } }
</q-html>
q-template instances render their template nodes directly (no runtime host element, no this.component).
<q-html>
q-template badge {
span { class: "badge" slot { label } }
}
badge { label { text { New } } }
</q-html>
window.QHTML_RUNTIME_DEBUG = true;document.querySelector("q-html").update();document.querySelector("q-html").invalidate({ forceBindings: true });q-component my-comp {
q-logger { q-signal q-property }
q-property count: 0
q-signal ping(value)
}
q-loggerscope is lexical:- inside a
q-componentdefinition: applies to all instances of that component - inside a specific instance block: applies to that instance only
- inside a
- Supported categories:
q-property,q-signal,q-component,function,slot,model,instantiation,all
q-perf is direct-child instrumentation metadata. It does not render a DOM element and it is not inherited by descendants. Add it to the exact component, worker, element, or host scope you want to measure.
q-component measured-counter {
q-perf { q-timer q-property q-signal function }
q-property count: 0
q-signal changed(value)
function step() {
this.component.count = Number(this.component.count) + 1;
this.component.changed(this.component.count);
}
q-timer tick {
interval: 100
repeat: true
running: true
ontimeout { this.component.step(); }
}
}
measured-counter counterA { }
Supported flags:
q-timer: timer timeout execution.q-signal: signal emission.q-property: declared property writes.q-worker: worker startup and worker method dispatch.function: component method calls.all: all q-perf supported categories.
Runtime stores aggregated counters on the measured QDOM object as perf_data:
{
totalMs: 12.5,
count: 4,
averageMs: 3.125,
metrics: {
"q-timer": { totalMs: 10, count: 2, averageMs: 5 }
}
}After QHTMLContentLoaded, QHTML logs each measured node as { uuid, canonicalName, referenceName, perf_data }.
These libraries are now obsolete as their functionality has been fully merged into the core modules through various means.
While they will continue to work, it is recommended to use q-style and q-theme instead for simplicity and ease of implementation.
But for those who want to use the obsolete libraries:
These scripts register custom elements like w3-card and bs-btn so you can use them as tag names. They apply CSS classes to their first non-w3-* / non-bs-* descendant and then remove the custom wrapper elements.
<link rel="stylesheet" href="w3.css" />
<script src="w3-tags.js"></script><q-html>
w3-card,w3-padding {
div { text { This div receives W3 classes. } }
}
</q-html>
<q-html>
q-style padded-card {
q-style-class { w3-card w3-padding }
}
q-theme main-theme {
div { padded-card }
}
main-theme { div { text { This div receives W3 classes } } }
</q-html>
<link rel="stylesheet" href="bs.css" />
<script src="bs-tags.js"></script><q-html>
bs-btn,bs-btn-primary {
button { text { Primary button } }
}
</q-html>
modules/qdom-core/README.mdmodules/qhtml-parser/README.mdmodules/dom-renderer/README.mdmodules/qhtml-runtime/README.mdmodules/release-bundle/README.md
Use \{ and \} when you want literal braces inside block bodies.
<q-html>
div {
text { hello \} world }
}
</q-html>
Resulting HTML:
<q-html>
<div>hello } world</div>
</q-html>