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.
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:
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
<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.
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>
Comma chains create nested elements:
div,section,h3 { text { Nested } }
Resulting HTML:
<div>
<section>
<h3>Nested</h3>
</section>
</div>
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>
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>
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.
p {
style { font-size: 20px; margin: 0; }
text { Plain text content }
}
Resulting HTML:
<p style="font-size: 20px; margin: 0;">Plain text content</p>
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:
q-component block defines the component type.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.
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.
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.
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.
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 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.
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:
this.statusText resolves on the nearest component instance.panelA.statusText dot-walks through the named component instance.statusText, which shadows the inherited name only for that child branch.parent() / parentComponent() from mounted DOM helpers, or .parent() / .qhtmlParent from QHTML node objects.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 } }
}
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>
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.
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.
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 { }
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.
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.
QHTML7 can use raw style { ... } blocks, but reusable styling is usually better with q-style and q-theme.
q-styleq-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-themeThemes 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 } }
}
}
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-themeq-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-transitionq-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-animationq-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%
}
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:
width, heightminWidth, minHeightmaxWidth, maxHeightgapflexwrapminColWidthstretch / layoutStretchsizeHint / layoutHintUse 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" }
}
q-timerq-timer clock {
interval: 1000
repeat: true
running: true
ontimeout {
console.log("tick");
}
}
Behavior:
repeat: true uses interval behavior.repeat: false uses timeout behavior.q-canvasq-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-emitterparticle-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:
start()stop()clear()burst(x, y, count)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.
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.
var someobject = new JavascriptClass() which you can expose to QHTML by calling qhtmlObject.setContextProperty("someobject", someobject); followed by qhtmlObject.render() to re-evaluate the qhtmlObject with the new context property defined.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>
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:
background { painterName }border { painterName }mask { painterName }Paint handlers may also appear as onPaintBackground, onPaintBorder, and onPaintMask where supported by the runtime.
QHTML7 stores the persistent document model as a QHTMLDomTree.
Common flow:
.fromQHTML(source)..toQHTML(), .toHTML(), .toJSON(), or .toJSONText()..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>
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:
.childList().childCount().childAt(index).findChildrenByType(typeName).sourceQHTML() / .toQHTML().renderHtml() / .toHTML().toJSON() / .toJSONText().fromJSON(value) / .fromJSONText(text).setPropertyText(name, value).remove() / .remove(child)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 <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.
tools/editor.html: browser QHTML editor.tools/layout-builder.html: visual editor for q-layout, q-row, and q-col.tools/page-builder.html: page builder shell using the shared layout editor and palette.tools/particle-editor.html: particle-emitter editor/exporter.tools/roller.html: converts QHTML-containing HTML files toward static HTML using .toHTML().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
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-loggerq-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:
q-propertyq-signalq-componentq-slotfunctionallLogging is for development and can be removed from production QHTML.
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.
Use \{ and \} for literal braces inside block content.
div {
text { hello \} world }
}
Resulting HTML:
<div>hello } world</div>