Skip to content

qhtml/qhtml6

Repository files navigation

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.js v6.9.7

QHTML is a compact language and runtime for building web UIs with readable block syntax, reusable components, signals, and live QDOM editing.

Whats New in v6.9.7

  • Bumped the release line to 6.9.7.
  • Added QML-style behavior on <property> with NumberAnimation as 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-animation for CSS motion-path animation sugar inside q-style.
  • Added q-slot-default for component slot fallback content and updated the UI component defaults that use it.
  • Added q-ui-split-layout and default slot content for the optional q-ui-components.qhtml component set.

1. Quick Start

Project setup

  1. Clone qhtml6 github repository
git clone https://github.com/qhtml/qhtml6.git
  1. Copy qhtml6 into your project qhtml folder
source ./deploy.sh /path/to/project

1. Include qhtml.js

In 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

2. Write QHTML in a <q-html> tag

<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>

2. Core Syntax

Elements and nesting

<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>

Selector chains (creates nested elements)

<q-html>
  div,section,h3 { text { Nested } }
</q-html>

Resulting HTML:

<q-html>
  <div><section><h3>Nested</h3></section></div>
</q-html>

Class and id shorthand

<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 and NumberAnimation

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 { }

Attributes

<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>

text, html, and style blocks

<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>

6. Components

q-component defines a runtime host element with:

  • q-property fields
  • function ... {} methods on this.component
  • q-signal ... signals on this.component
  • q-alias ... { ... } computed alias properties on this.component
  • slot { name } placeholders for projection

Minimal component with properties and a slot

<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>

Slot defaults

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 { }
}

q-component instantiation (typed named-instance syntax)

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

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.

Scope and context for named instances

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-property syntax

q-component my-comp {
  q-property title
  q-property selected: true
}

q-property reference defaults (bound path syntax)

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.prop1 resolves to "testing".
  • mycomp2.prop2 resolves 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.

on<Property>Changed auto-signals

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:

  • onCountChanged fires only when count changes to a different value.
  • Payload is available at event.detail.params.value and event.detail.args[0].
  • Each declared property also gets an implicit runtime signal function (countChanged, titleChanged, etc), so you can use .connect(...) in addition to on<Property>Changed.
  • on<Property>Changed matching is case-insensitive (oncountchanged, onCountChanged, onCoUnTcHaNgEd all work).

q-array and q-map as property values

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.

Inline property assignment

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.enabled returns true

Named declarations and reuse

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 { }

Accessing them from JavaScript

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 basics

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 basics

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} } }
}

for keyword (template iteration)

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-model helpers 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:

  • for expression scope follows runtime inline expression rules.
  • For component state, prefer explicit component references (this.component.<name>).

q-timer (keyword-level timer)

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: true uses native setInterval(...).
  • repeat: false uses native setTimeout(...).
  • The named timer handle is exported globally as window.<name> (for example window.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 timeout signal 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-timer declarations 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 (scoped runtime variable)

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 / switch (named lookup function)

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 (keyword-level canvas)

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 as window.<name> and host-scoped <name>.
  • <name>.context points to the 2d rendering context for that specific canvas.
  • Canvas rendering can be timer-driven (q-timer) or signal-driven depending on your component flow.

particle-emitter / q-particle-emitter (canvas-backed particle effects)

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 while running is true.
  • running: boolean emission switch; assigning emitter.running = true/false updates the running attribute.
  • 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.
  • maxParticles or stopAfter: optional total particle limit before auto-stopping.
  • src, mask, color, colorOpacity / color-opacity: sprite source, optional per-particle alpha mask, and fallback/tint color. When src is present, the source image remains visible; color is applied as a transparent overlay or masked backdrop instead of replacing the image pixels. Set colorOpacity / color-opacity to 0 to 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): queues num particles emitted at the current emitRate from the supplied origin. The emitter's configured x / y attributes are not changed.

See doc/11-particle-emitter/ for the full attribute reference.

q-spritesheet [alpha] (component-level state machine)

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 frameWidth is missing/invalid, runtime derives frameWidth = imageWidth / frameCount.
  • q-spritesheet includes an internal q-timer and advances frames via declarative wiring: q-connect { frameTimer.timeout this.component.nextFrame }.
  • Control methods: start(), stop(), nextFrame().

q-worker (component-level background worker)

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-property and q-signal declarations are supported in worker scope.
  • Signals emitted from worker methods re-enter normal component signal handling (on<Signal> / .connect(...)).

q-tree-view

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.

extends syntax

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-property
  • function ... { }
  • q-signal
  • q-alias
  • onReady and other lifecycle hooks
  • slot { ... } placeholders
  • template children / rendered markup

Child components are merged after parent components, so child methods and declarations with the same name win.

Single inheritance

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.

Multiple inheritance

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.

Merge order

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-a
  • base-b
  • final-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 syntax (component-level WebAssembly bridge)

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-wasm is valid only inside q-component.
  • mode: worker is default; if worker mode cannot be used, runtime falls back to main thread.
  • allowImports { ... } is supported in main-thread mode.

Bind a named child node to a component property (property <name> { ... })

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-alias syntax

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 syntax (pre-parse inline expansion)

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 } }
}

Scoped ${reference} placeholders

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.

Events and Lifecycle

Inline events

<q-html>
  button {
    text { Click }
    onclick { this.textContent = "Clicked"; }
  }
</q-html>

Resulting HTML:

<q-html>
  <button onclick="this.textContent = &quot;Clicked&quot;;">Click</button>
</q-html>

Lifecycle blocks

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>

then { ... } chaining after on* blocks

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>

Scoped $() selector shortcut

Use $("<selector>") inside QHTML runtime JavaScript to query within the current <q-html> root only.

  • $("#sender") is equivalent to this.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)

}

Styles and Themes

q-style + q-theme are the preferred styling model for new code.

q-style with class import (q-style-class)

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-class merges class names into the element class attribute.
  • Inline q-style declarations are still applied via style="".
  • If both class CSS and inline declarations target the same property, inline wins.

q-transition + q-style-transition

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:

  • timing accepts CSS timing-function values directly (for example ease, ease-in-out, cubic-bezier(...)).
  • Numeric duration / delay values are interpreted as milliseconds (3000 -> 3000ms).
  • Multiple transition references can be listed in q-style-transition and are combined into a comma-separated CSS transition value.

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

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 positioning

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 + q-style-painter (Paint Worklet)

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-property entries in q-painter are exposed to onpaint as this.<property>.
  • onpaint does not take parameters; use this.width, this.height, and painter properties.
  • If CSS.paintWorklet is unavailable, QHTML logs a warning and skips painter attachment.

Basic reusable style

q-style panel {
  backgroundColor: #eff6ff
  color: #1e293b
  border: 1px solid #93c5fd
}

Apply style directly in selector chain

panel,div { text { Styled panel } }

Use q-style-class for utility-class composition

q-style card-shell {
  q-style-class { w3-card w3-round-large w3-padding }
  borderColor: #cbd5e1
}

Theme maps selectors to styles

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 fallback layer

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 }
}

Scoped theme application

article-theme {
  div {
    h3 { text { Title } }
    p  { text { Description } }
  }
}

Compose themes

q-theme base-theme {
  button { button-base }
}

q-theme admin-theme {
  base-theme { }
  .danger { button-danger }
}

Override class CSS with inline style declaration

q-style button-base {
  q-style-class { w3-button w3-round }
  backgroundColor: #0f766e
  color: #ffffff
}

Notes:

  • q-style-class merges into the element class attribute.
  • Inline declarations from q-style are written to style="" and win on property conflicts.
  • Themes can be declared once and reused as lightweight styling scopes.

4. State with q-script (q-bind alias/deprecated)

q-bind is deprecated and treated the same as q-script. Use q-script for assignment expressions.

Bind to text

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 { }

5. q-script

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.

Inline replacement

<q-html>
  div {
    q-script { return "p { text { Inserted by q-script } }"; }
  }
</q-html>

Assignment form

<q-html>
  div {
    data-note: q-script { return "n:" + (4 + 1) }
    text { q-script { return "script-inline"; } }
  }
</q-html>

6. ${expression} inline expressions

${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).

Works in rendered text/attribute strings

<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} } }
}

Macro slot placeholders (scoped)

q-macro badge {
  slot { label }
  return { span { text { ${label} } } }
}

Cannot be used for keyword-level symbols

q-component ${dynamicName} { }     // invalid
q-keyword ${alias} { q-component } // invalid
${tagName} { text { hi } }         // invalid

Deprecated alias (q-bind)

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 syntax (scoped keyword aliasing)

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.

7. Signals

There are two signal forms: component signals (function-style) and QHTML signal definitions.

7.1 Component signals (function-style)

<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>

7.2 q-signal name { ... } definitions (slot payload signals)

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>

7.3 Signal use cases (direct emit + imperative connect)

<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>

7.4 Signal use case (q-connect declarative wiring)

<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.

8. State Machines

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.

8.1 Basic two-state machine

<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:

  • mymachine is a named component instance.
  • mymachine.state is a declared q-property.
  • The first state is the initial state unless you set state: "stateName" in the machine body.
  • Assigning mymachine.state renders only that state's QHTML inside the <q-state-machine> host.

8.2 State change signals with q-connect

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);
}

8.3 Component members on the machine host

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-property stores machine-level values.
  • function adds methods to the <q-state-machine> host.
  • q-signal creates normal connectable signal functions.
  • State blocks stay declarative and contain the QHTML rendered for each state.

8.4 Named instances inside states

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.

9. q-rewrite

q-rewrite is a pre-parse macro that expands calls like name { ... } before the rest of QHTML is parsed.

Template-style rewrite

q-rewrite pill {
  slot { label }
  span { class: "pill" slot { label } }
}

pill { label { text { OK } } }

Return-style rewrite (this.qdom().slot("name"))

q-rewrite choose-class {
  slot { active }
  return {
    q-script {
      return this.qdom().slot("active").trim() === "true" ? "on" : "off";
    }
  }
}

div { class: choose-class { active { true } } }

10. QDOM API

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).

Find and append

<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>

Replace a node with QHTML

const host = document.querySelector("q-html");
host.qdom().find("#items").replaceWithQHTML("ul { li { text { Replaced } } }");
host.update();

qdom().rewrite(...) (QDOM-side equivalent of a q-rewrite)

document.querySelector("q-html").qdom().find("#items").rewrite(function () {
  return "div { class: 'box' text { Rewritten } }";
});
document.querySelector("q-html").update();

Serialize / Deserialize

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();

Component instance property helpers

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 scope

Scoped vs full updates

this.component.update();        // this component subtree
this.component.root().update(); // whole <q-html>

11. Builder and Editor

  • dist/demo.html is the component usage gallery.
  • q-editor supports authoring live QHTML and previewing output.
  • q-builder provides visual inspect/edit workflows on mounted <q-html> content.

<q-editor> (inline QHTML source)

<q-editor> takes QHTML as literal text content (not nested <q-html>).

<q-editor>
  h3 { text { Hello from q-editor } }
</q-editor>

q-import { ... } (include QHTML files)

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 (compile-time pure 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>

12. Debug Tips

window.QHTML_RUNTIME_DEBUG = true;
document.querySelector("q-html").update();
document.querySelector("q-html").invalidate({ forceBindings: true });

q-logger (scoped debug logging)

q-component my-comp {
  q-logger { q-signal q-property }
  q-property count: 0
  q-signal ping(value)
}
  • q-logger scope is lexical:
    • inside a q-component definition: applies to all instances of that component
    • inside a specific instance block: applies to that instance only
  • Supported categories: q-property, q-signal, q-component, function, slot, model, instantiation, all

q-perf (QDOM performance counters)

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 }.

13. Optional Tag Libraries (w3-tags.js, bs-tags.js) [DEPRECATED]

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.

W3CSS tags

<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-style equivalent

 <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>

Bootstrap tags

<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>

14. Module READMEs

  • modules/qdom-core/README.md
  • modules/qhtml-parser/README.md
  • modules/dom-renderer/README.md
  • modules/qhtml-runtime/README.md
  • modules/release-bundle/README.md

Escape sequences

Escaping { and } in block content

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>

About

Official Repository for qhtml6 - Modern Language Replacement for HTML

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages