qhtml7

QHTML7

QHTML7 is a native JavaScript implementation of the QHTML language: a declarative way to describe HTML, CSS, component structure, runtime properties, signals, layout, and browser-facing behavior in one readable source format.

Language design, specifications, and tests created by humans; implementation by ChatGPT 5.6 Codex.

What’s New

7.4.6: QHTML-native dynamic creation and removal

QHTML components can now be instantiated directly from their component definition with .create(parent, properties). The first argument is the QHTML parent object to append into, and the optional second argument supplies initial property assignments. The parent is rerendered after the new instance is added.

q-component card {
  q-property title: "New card"
  div.card,text { ${this.title} }
}

q-component panel {
  div container { }

  button {
    onclick { card.create(container, { title: "Created at runtime" }) }
    text { Add card }
  }
}

panel app { }

Any named QHTML object can now be removed with .remove(). Calling .remove(child) on a parent removes that specific child from the parent’s qhtmlChildren. Calling object().remove() from an event handler removes the current rendered QHTML object, not just the browser DOM element.

q-component removableCard {
  div.card,text { Remove me }
}

q-component removalPanel {
  removableCard card1 { }

  button {
    onclick { card1.remove() }
    text { Remove named card }
  }

  div wrapper1 {
    button {
      onclick { object().remove() }
      text { Remove this button }
    }
  }
}

removalPanel app { }

Plain DOM elements can also use named QHTML syntax. A declaration such as div wrapper1 { ... } still renders a normal <div>, but wrapper1 is preserved as the QHTML reference name for dot-walking, .toQHTML(), .create(), and .remove() workflows. Named objects declared inside component slots are now also available to the containing component context.

Useful public entry points:

1. Quick Start And HTML Syntax

Install QHTML7

Copy the files in dist/ to your web server:

/path/to/site/dist/qhtml.js

Then include the entry point:

<script src="/dist/qhtml.js"></script>

qhtml.js is the QHTML7 native JavaScript runtime bundle. Use a real HTTP server; browser filesystem loading is not a supported runtime environment.

For local development from this repo:

python3 -m http.server

Then open pages such as:

http://127.0.0.1:8000/test/demo.html
http://127.0.0.1:8000/tools/editor.html

Write QHTML In <q-html>

<script src="/dist/qhtml.js"></script>

<q-html>
  h1 { text { Hello QHTML7 } }
  p { text { Your first QHTML7 render is running. } }
</q-html>

Resulting HTML:

<h1>Hello QHTML7</h1>
<p>Your first QHTML7 render is running.</p>

text { ... } creates escaped text content. html { ... } inserts raw HTML.

Elements And Nesting

Plain HTML nodes use the tag name followed by a block:

section {
  h2 { text { Overview } }
  p  { text { A readable UI tree. } }
}

Resulting HTML:

<section>
  <h2>Overview</h2>
  <p>A readable UI tree.</p>
</section>

Selector Chains

Comma chains create nested elements:

div,section,h3 { text { Nested } }

Resulting HTML:

<div>
  <section>
    <h3>Nested</h3>
  </section>
</div>

Class And ID Shorthand

div#main {
  div.card {
    p#body.large { text { Card body } }
  }
}

Resulting HTML:

<div id="main">
  <div class="card">
    <p id="body" class="large">Card body</p>
  </div>
</div>

Attributes

Assignments inside ordinary HTML blocks become HTML attributes unless the assignment is a known CSS shortcut or a declared QHTML property.

a {
  href: "https://example.com"
  target: "_blank"
  text { Open Example }
}

Resulting HTML:

<a href="https://example.com" target="_blank">Open Example</a>

Unknown/custom elements work the same way:

my-element {
  someattribute: "1"
  otherthing: "2"
}

Resulting HTML:

<my-element someattribute="1" otherthing="2"></my-element>

Style Values

Use real CSS values. QHTML7 does not guess missing CSS units for you.

div#box {
  width: "100px"
  height: "50vh"
  position: "absolute"
  left: "20px"
  top: "10px"
  text { CSS-ready values }
}

Resulting HTML:

<div id="box" style="width: 100px; height: 50vh; position: absolute; left: 20px; top: 10px;">CSS-ready values</div>

This is intentionally invalid:

div { width: 400 }

The browser receives width:400, which is invalid CSS for width, and QHTML7 logs a warning such as:

Invalid CSS width property: 400

Unitless CSS properties such as opacity, zIndex, order, and flexGrow can still use unitless numbers.

Raw Style Blocks

p {
  style { font-size: 20px; margin: 0; }
  text { Plain text content }
}

Resulting HTML:

<p style="font-size: 20px; margin: 0;">Plain text content</p>

2. Q-Components

q-component defines a reusable QHTML component type. A component is a named QHTML template with its own properties, functions, signals, slots, and rendered DOM structure.

Think of it as a QHTMLDomTree-backed custom component definition:

Define A Component

q-component info-card {
  article.card {
    h3 { text { Info } }
    p { text { This content came from a component. } }
  }
}

This only defines info-card. It does not render anything until it is instantiated.

Instantiate A Component

q-component info-card {
  article.card {
    h3 { text { Info } }
    p { text { This content came from a component. } }
  }
}

info-card { }

Simplified resulting HTML:

<info-card>
  <article class="card">
    <h3>Info</h3>
    <p>This content came from a component.</p>
  </article>
</info-card>

The actual DOM may include QHTML runtime attributes such as component-instance and qhtml-node. Those are runtime bookkeeping attributes and are omitted from examples.

Named Instances

Instances may be named. Named instances become symbols that other QHTML code can reference.

q-component status-pill {
  span.status { text { Ready } }
}

status-pill headerStatus { }

The instance name is headerStatus.

Define Properties

Use q-property inside a component definition:

q-component badge {
  q-property label: "New"
  q-property widthAmount: 40%

  span.badge {
    text { ${label} }
  }

  onwidthAmountchanged(value) {
    this.querySelector(".badge").style.width = value;
  }
}

badge { }
badge { label: "Updated" }

Simplified resulting HTML:

<badge>
  <span class="badge">New</span>
</badge>

<badge>
  <span class="badge">Updated</span>
</badge>

Instance assignments override the component’s default property values.

Access Properties From An Instance

Named component instances can be referenced by name:

q-component source-box {
  q-property title: "Copied title"
}

source-box source1 { }

q-component target-box {
  q-property copiedTitle: source1.title

  div.target {
    text { ${copiedTitle} }
  }
}

target-box { }

Simplified resulting HTML:

<source-box></source-box>

<target-box>
  <div class="target">Copied title</div>
</target-box>

Simple property references on the right-hand side are live bindings. If source1.title changes later, target-box.copiedTitle is updated until copiedTitle is assigned directly.

Inside a component, this refers to the nearest parent component instance in JavaScript handlers, functions, property handlers, signal handlers, animations, and event blocks:

q-component counter-card {
  q-property count: 0

  onready {
    this.count = this.count + 1;
  }

  oncountchanged(value) {
    this.querySelector(".count").textContent = String(value);
  }

  div.count { text { 0 } }
}

counter-card { }

Functions

Functions are declared inside a component and are callable on the instance.

q-component action-card {
  q-property label: "waiting"

  function markDone() {
    this.label = "done";
  }

  onlabelchanged(value) {
    this.querySelector(".state").textContent = value;
  }

  button {
    text { Mark done }
    onclick {
      this.markDone();
    }
  }

  div.state { text { waiting } }
}

action-card card1 { }

Functions are runtime behavior, so there is no special static HTML output to show beyond the rendered button and state node.

Component Scope And Named References

QHTML scope is top-down. A parent component’s properties and named references are visible to its descendants unless a local object shadows the same name.

q-component status-panel {
  q-property statusText: "ready"

  div {
    text { ${statusText} }
  }
}

q-component dashboard-card {
  q-property statusText: "loading"

  status-panel panelA { }

  div {
    text { ${panelA.statusText} }
  }
}

dashboard-card { }

Rules to keep in mind:

Slots

Slots let a component define insertion points for caller-provided content.

q-component panel-box {
  section.panel { h3 { text { Panel shell } } 
  div.panel-body {

      slot body {  }

    }
  }
}

panel-box {

  body {

    p { text { Projected content } }

  }
}

Simplified resulting HTML:

<panel-box>
  <section class="panel">
    <h3>Panel shell</h3>
    <div class="panel-body">
      <p>Projected content</p>
    </div>
  </section>
</panel-box>

Slot names are just child block names on the component instance. Here the component declares slot { body }, and the instance provides:

body {
  p { text { Projected content } }
}

Slot Defaults

Use q-slot-default to provide fallback content when the caller does not supply the slot.

q-component notice-card {
   slot body {
      p { text { Default notice } }
   }

  article.notice {
    slot body { }
  }
}

notice-card { }

notice-card {
  body {
    p { text { Custom notice } }
  }
}

Simplified resulting HTML:

<notice-card>
  <article class="notice">
    <p>Default notice</p>
  </article>
</notice-card>

<notice-card>
  <article class="notice">
    <p>Custom notice</p>
  </article>
</notice-card>

Signals

q-signal declares an instance signal. Call it like a function from component code. Handle it with on<signalName>.

q-component signal-card {
  q-signal sent(message)

  onsent(message) {
    this.querySelector(".out").textContent = String(message);
  }

  onready {
    this.sent("Signal received");
  }

  div.out { text { waiting } }
}

signal-card { }

Signals are runtime behavior, not static HTML. They are useful for component-local events and for connecting components together.

Events And Event Listeners

q-event declares a named action/event object in QHTML context. It is callable like a function from QHTML scripts, but it also dispatches a DOM CustomEvent named qhtml:<EventName>. This makes it useful for game flow, application-level actions, and coordination between components that are not direct parents or children.

q-component game-root {
  q-event StartGame(playerId, seed) { }
  q-event SwitchTurns(activePlayer) { }

  q-event-listener StartGame(playerId, seed) {
    this.querySelector(".status").textContent = "Player " + playerId + " seed " + seed;
  }

  q-event-listener SwitchTurns(activePlayer) {
    this.querySelector(".turn").textContent = activePlayer;
  }

  button {
    onclick {
      StartGame("player", 1234);
      SwitchTurns("enemy");
    }
    text { Start }
  }

  div.status { text { waiting } }
  div.turn { text { player } }
}

game-root { }

Simplified resulting HTML:

<game-root>
  <button>Start</button>
  <div class="status">waiting</div>
  <div class="turn">player</div>
</game-root>

The event declarations and listeners do not render DOM of their own. They become runtime objects and script bindings. When StartGame("player", 1234) runs, QHTML dispatches:

new CustomEvent("qhtml:StartGame", {
  bubbles: true,
  composed: true,
  detail: {
    name: "StartGame",
    eventName: "StartGame",
    args: ["player", 1234],
    parameters: { playerId: "player", seed: 1234 }
  }
});

Plain JavaScript can listen to QHTML events:

document.addEventListener("qhtml:StartGame", (event) => {
  console.log(event.detail.parameters.playerId);
});

Plain JavaScript can also dispatch into QHTML:

document.dispatchEvent(new CustomEvent("qhtml:SwitchTurns", {
  bubbles: true,
  composed: true,
  detail: {
    args: ["player"]
  }
}));

Use q-signal when an object/component is announcing something about itself. Use q-event when you want a named action bus in context that both QHTML and normal DOM JavaScript can call or observe.

Connect Signals To Functions

q-connect connects a signal source to a callable target.

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.value = message;
  }

  onvaluechanged(value) {
    this.querySelector(".out").textContent = String(value);
  }

  div.out { text { waiting } }
}

sender-box sender1 { }
receiver-box receiver1 { }

q-connect { sender1.sent receiver1.onMessage }

q-component driver {
  onready {
    sender1.sendNow("Connected");
  }
}

driver { }

Component Inheritance

Components can extend other components:

q-component base-card {
  q-property title: "Base"

  article.card {
    h3 { text { ${title} } }
    slot { body }
  }
}

q-component warning-card extends base-card {
  q-property title: "Warning"
}

warning-card {
  body {
    p { text { Be careful. } }
  }
}

Use inheritance when a component should share structure, properties, functions, or signals with a base component.

3. Imports And Component Files

Use q-import to include another QHTML file before rendering the current source.

q-import { ./shared/cards.qhtml }

info-card { title: "Imported component" }

Imports are resolved relative to the host page URL unless the path is absolute.

The distributed component set lives in dist/q-components/ and can be imported by path:

q-import { ../dist/q-components/q-sidebar.qhtml }
q-import { ../dist/q-components/q-tabs.qhtml }
q-import { ../dist/q-components/q-modal.qhtml }

q-import fetches QHTML resources with the runtime version appended as a query string, so component files are naturally refreshed when the QHTML runtime version changes.

q-require { ... } is available for resource-style requirements, but most user code should prefer q-import.

4. Styles, Themes, And Transitions

QHTML7 can use raw style { ... } blocks, but reusable styling is usually better with q-style and q-theme.

q-style

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

panel,div {
  text { Styled panel }
}

Simplified resulting HTML:

<div style="background-color: #eff6ff; color: #1e293b; border: 1px solid #93c5fd; padding: 16px; border-radius: 8px;">Styled panel</div>

q-theme

Themes map selectors to styles.

q-style title-accent { color: #1d4ed8 }
q-style body-muted   { color: #64748b }

q-theme article-theme {
  h3 { title-accent }
  p  { body-muted }
}

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

Anonymous Styles In Themes

q-component themed-panel {
  q-property accentColor: #1d4ed8

  q-theme card-theme {
    h3 { q-style { color: ${this.accentColor} } }
    .summary { q-style { color: #334155 } }
  }

  card-theme {
    section {
      h3 { text { Dynamic theme color } }
      p.summary { text { Theme values react to q-property changes. } }
    }
  }

  button {
    onclick { this.accentColor = "#dc2626"; }
    text { Change accent }
  }
}

themed-panel panel1 { }

q-default-theme

q-default-theme is a fallback layer. It applies first; later scoped q-theme rules override conflicts.

q-style panel-base { backgroundColor: #eef3fb color: #0f172a }
q-style panel-hot  { backgroundColor: #ffedd5 color: #7c2d12 }

q-default-theme base-theme {
  .card { panel-base }
}

q-theme demo-theme {
  base-theme { }
  .card { panel-hot }
}

q-transition

q-transition defines a named CSS transition. Apply it directly in an element or through a style/theme.

q-transition soft-change {
  duration { 300 }
  timing { ease-in-out }
  delay { 0 }
}

div.card {
  soft-change { opacity color paddingTop }
  opacity: 0.8
  text { Transition-ready card }
}

Through q-style-transition:

q-transition fade-in {
  property { opacity }
  duration { 300 }
  timing { ease-in-out }
}

q-style panel-style {
  q-style-transition { fade-in }
}

q-property-animation

q-property-animation animates a QHTML or DOM property. The compact form puts the target object and target property in the same expression:

q-component progressBar {
  q-property amount: 40%

  onamountchanged(value) {
    this.querySelector(".fill").style.width = value;
  }

  div.track {
    width: "120px"
    height: "12px"
    backgroundColor: "black"

    div.fill {
      width: "40%"
      height: "12px"
      backgroundColor: "orange"
    }
  }
}

progressBar bar1 { amount: 10% }

q-property-animation growBar {
  target: bar1.amount
  from: 10%
  to: 75%
  duration: 900
  steps: 30
  running: true
  emitInterpolatedValues: true
}

The older split form is still valid:

q-property-animation growBar {
  target: bar1
  property: "amount"
  from: 10%
  to: 75%
}

5. Layout

q-layout, q-row, and q-col are built-in layout nodes. They render as normal DOM layout containers and are also the model used by the visual layout/page builder tools.

q-layout {
  width: "100%"
  height: "80vh"
  gap: "12px"

  q-row {
    height: "20vh"

    q-col {
      width: "20vw"
      text { Left column }
    }

    q-col {
      width: "60vw"
      text { Main column }
    }
  }
}

q-layout and q-col stack children vertically by default. q-row flows horizontally.

Useful layout properties:

Use CSS values with units where needed:

q-row {
  width: "100%"
  gap: "16px"
  wrap: "wrap"

  q-col { width: "20vw" minWidth: "12rem" }
  q-col { width: "40vw" minWidth: "18rem" }
}

6. Runtime Keywords

q-timer

q-timer clock {
  interval: 1000
  repeat: true
  running: true

  ontimeout {
    console.log("tick");
  }
}

Behavior:

q-canvas

q-canvas board {
  width: 320
  height: 180
}

button {
  text { Draw }
  onclick {
    board.context.clearRect(0, 0, 320, 180);
    board.context.fillStyle = "rgba(16,185,129,0.9)";
    board.context.fillRect(20, 20, 120, 80);
  }
}

q-canvas <name> exports a named canvas handle. <name>.context is the 2D context.

particle-emitter

particle-emitter is a QHTML7-provided custom element for canvas-backed particles. It does not require loading particle-emitter.js manually.

div#energy-field {
  style {
    position: relative;
    width: 420px;
    height: 220px;
    overflow: hidden;
    background: #07111f;
  }

  particle-emitter energyEmitter {
    id: "energy-emitter"
    running: true
    src: "tools/assets/particle.png"
    emitRate: 84
    interval: 18
    lifetime: 3600
    lifetimeVariation: 900
    x: 210
    y: 214
    xVariation: 145
    yVariation: 8
    yVelocity: -1.25
    startSize: 10
    endSize: 30
    startOpacity: 0.35
    endOpacity: 0.02
    maxActiveParticles: 96
  }
}

Useful methods on the DOM element:

7. Scripts, Events, And Expressions

Event Handlers

DOM event handlers use on<event> blocks. The handler executes in the nearest parent component instance scope, so this is the component instance, not the clicked helper node:

button {
  text { Click me }
  onclick {
    this.querySelector("button").textContent = "Clicked";
  }
}

Use normal DOM APIs from the component instance when you need browser output, for example this.querySelector(...), this.setAttribute(...), or CSS shortcut properties.

Interpolation And setContextProperty

${expression} evaluates inside text/attribute strings.

You can call object1.setContextProperty(propertyName, object2) as long as object2 is in scope and object1 is in scope.

div#mydiv {
  title: "Current user: ${currentUser}"
  text { Hello ${currentUser} }
}

Interpolated values are reactive when they reference QHTML properties such as ${label}, ${this.label}, or ${objectName.label}. QHTML installs a hidden handler for the referenced property-change signal and re-renders the affected text/html/attribute/style value.

Plain JavaScript context values supplied by setContextProperty() still require render() after changing the context pointer.


document.querySelector("#mydiv").setContextProperty("currentUser", "myUserA")
document.querySelector("#mydiv").render()

The result is

<div id="mydiv" title="Current User: myUserA">Hello myUserA</div>

8. Paint And Houdini

q-painter defines a reusable paint worklet body with QHTML-owned defaults.

q-painter panel-painter {
  q-property fill: "rgba(40,80,160,0.9)"

  onpaint {
    this.fillStyle = this.fill;
    this.fillRect(0, 0, this.width, this.height);
  }
}

q-style panel-style {
  width: "180px"
  height: "48px"

  q-style-painter {
    background { panel-painter }
  }
}

panel-style,div {
  text { Painted }
}

q-style-painter supports:

Paint handlers may also appear as onPaintBackground, onPaintBorder, and onPaintMask where supported by the runtime.

9. QHTMLDomTree API

QHTML7 stores the persistent document model as a QHTMLDomTree.

Common flow:

  1. Create a tree.
  2. Load source with .fromQHTML(source).
  3. Inspect or mutate QHTML node objects.
  4. Serialize with .toQHTML(), .toHTML(), .toJSON(), or .toJSONText().
  5. Load serialized state with .fromJSON(value), .fromJSONText(text), or .fromQHTML(source).
<script src="/dist/qhtml.js"></script>
<script>
document.addEventListener("QHTML7Ready", function () {
  const tree = new QHTMLDomTree();
  tree.fromQHTML('section { h2 { text { Hello } } }');

  console.log(tree.toHTML());
  console.log(tree.toQHTML());
  console.log(tree.toJSON());
});
</script>

Traversal

Every QHTMLDomNode-based object exposes child helpers:

const tree = new QHTMLDomTree();
tree.fromQHTML('q-layout { q-row { q-col { text { Cell } } } }');

const children = tree.childList();
const layouts = tree.findChildrenByType('QHTMLLayout');

Useful methods:

Component definition objects also expose .create(parent, properties):

const tree = new QHTMLDomTree();
tree.fromQHTML('q-component card { div,text { ${this.title} } } div container { }');

const card = tree.qhtmlResolve("card");
const container = tree.qhtmlResolve("container");
card.create(container, { title: "Runtime card" });

Mounted Host Helpers

Mounted <q-html> elements expose high-level helpers:

const host = document.querySelector("q-html");
host.fromQHTML('div { text { Replaced source } }');

console.log(host.toHTML());
console.log(host.toQHTML());

toHTML() is useful for static output and tooling. tools/roller.html can process HTML files containing <q-html> blocks and output HTML clones.

10. Tools

Shared layout builder files:

tools/layout-builder/main.qhtml
tools/layout-builder/main.js

Page builder palette files:

tools/page-builder/palette.qhtml
tools/page-builder/palette.js

11. Debugging

Runtime Events

Wait for the runtime:

document.addEventListener("QHTML7Ready", function (event) {
  console.log(event.detail);
});

Wait for mounted QHTML content:

document.addEventListener("QHTMLContentLoaded", function () {
  console.log("QHTML content loaded");
});

Enable runtime debug logging:

window.QHTML_RUNTIME_DEBUG = true;

q-logger

q-logger attaches a scoped logger to the current QHTML node.

q-component debug-card {
  q-logger { q-signal q-property }
  q-property count: 0
  q-signal ping(value)
}

Supported category names include:

Logging is for development and can be removed from production QHTML.

Common Problems

CSS length values need units:

div { width: "400px" }  // valid
div { width: 400 }      // invalid CSS width

Relative imports and assets are resolved from the page URL. If a copied test page breaks in tmp/, check its <script src>, q-import, and asset paths.

12. Escaping

Use \{ and \} for literal braces inside block content.

div {
  text { hello \} world }
}

Resulting HTML:

<div>hello } world</div>

Development Notes