cyghtml
v0.1.2
Published
Low-level JSON-driven overlay HTML renderer for Cygnus libraries.
Readme
cyghtml
cyghtml is a low-level JSON-driven overlay HTML renderer.
Its purpose is simple:
- create a top-most overlay root above an existing web page
- render HTML elements into that overlay from JSON
- apply CSS and CSS variables from JSON
- keep the runtime close to normal HTML and CSS semantics
- default to a non-blocking overlay root, while allowing an interactive root when the overlay itself should behave like an app UI
cyghtml is not a tutorial engine.
It does not own tutorial steps, scenes, tooltip presets, highlight presets, or tutorial progression logic.
Those belong in a higher-level library such as cygtut.
Philosophy
cyghtml follows these design rules.
- It is a renderer first.
- It should stay close to HTML and CSS.
- It should default to a non-blocking overlay root for page overlays, but allow an interactive overlay root when needed.
- It should avoid inventing new UI concepts unless absolutely necessary.
- It should accept final render data, not tutorial meaning.
- A higher-level library may compute positions, state, and tutorial rules, then pass the result into cyghtml.
- cyghtml should focus on overlay mounting, DOM creation, CSS application, parent-child composition, and final rendering.
In short:
cygtutdecides what to showcyghtmlshows it
Relationship To cygtut
The intended architecture is:
cygtut -> builds final overlay JSON -> cyghtml -> renders overlay DOMThat means:
cygtutcan calculate tutorial state, step changes, anchors, and presetscyghtmlreceives the final JSON tree and renders it- cyghtml should remain reusable outside tutorials
Top-Level Keys
A cyghtml document is built around three top-level keys.
varscsshtml
Example:
{
"vars": {
"--panel-x": "120px",
"--panel-y": "80px",
"--panel-bg": "#111827",
"--panel-color": "#ffffff"
},
"css": {
".panel": {
"position": "absolute",
"left": "var(--panel-x)",
"top": "var(--panel-y)",
"background": "var(--panel-bg)",
"color": "var(--panel-color)",
"padding": "16px",
"border-radius": "14px"
}
},
"html": [
{
"tag": "div",
"attrs": {
"id": "panel",
"class": "panel"
},
"appendTo": "root",
"items": [
{
"tag": "h3",
"text": "Login Guide"
}
]
}
]
}Top-Level vars
vars defines runtime variables.
In the first version, the most natural use is CSS variables, but the runtime may also keep these values available for later expansion.
Example:
{
"vars": {
"--panel-x": "120px",
"--panel-y": "80px",
"--panel-width": "280px",
"--panel-bg": "#111827"
}
}Recommended usage:
- CSS custom properties such as
--panel-x - values that higher-level systems want to inject before render
A higher-level library such as cygtut may rebuild the final JSON whenever its own state changes.
cyghtml itself does not need to become a full tutorial state engine.
Top-Level css
css defines CSS rules.
The simplest model is a JSON object whose keys are CSS selectors and whose values are CSS declarations.
Example:
{
"css": {
":root": {
"--panel-bg": "#111827"
},
".panel": {
"position": "absolute",
"left": "120px",
"top": "80px",
"width": "280px",
"padding": "16px",
"background": "var(--panel-bg)",
"color": "#ffffff"
},
".panel h3": {
"margin": "0 0 8px 0"
}
}
}What css Can Contain
Inside css:
- selector keys such as
.panel,#panel,:root,.panel button - declaration objects whose keys are CSS properties
- values written as normal CSS strings
Examples of valid declaration keys:
positionlefttopwidthheightpaddingmarginbackgroundbackground-colorborder-radiusfont-sizez-indexanimationtransform
The goal is to stay as close to normal CSS as possible.
Top-Level html
html defines the DOM that should be rendered into the overlay.
html is an array of node definitions.
Example:
{
"html": [
{
"tag": "div",
"attrs": {
"id": "panel",
"class": "panel"
},
"appendTo": "root",
"items": [
{
"tag": "h3",
"text": "Login Guide"
},
{
"tag": "button",
"text": "Next"
}
]
}
]
}html Node Model
Each entry inside top-level html is a node definition.
A node may use either:
tag- or
markup
Only one should be used for a single node.
If both are used, the result becomes ambiguous and should be treated as invalid authoring.
Common Node Keys
A node may contain:
tagmarkupattrsstyleappendToappendToClasshostAppendTohostAppendToClassitemstextevents
These keys are explained below.
tag
tag creates a normal HTML element.
Example:
{
"tag": "div"
}What tag Can Contain
tag is a string such as:
divspanbuttonph1h2h3imgsectionarticle- any valid HTML tag you want to support
tag Example
{
"tag": "button",
"attrs": {
"id": "next",
"type": "button"
},
"text": "Next"
}markup
markup lets a node be created from a raw HTML string.
Example:
{
"markup": "<div id='panel' class='panel'></div>"
}Rules For markup
- it should produce exactly one outer root element
- inline HTML attributes inside the markup itself are valid, including things such as
class,id,data-*, andstyle="..." attrs,style,appendTo,appendToClass,hostAppendTo,hostAppendToClass, anditemsapply to that outer root element- when both inline markup styles and outer
styleare present, the runtime may apply outerstylelast so it behaves like a final patch on the root element - it is useful when you want direct HTML syntax instead of
tagplusattrs
markup Example
{
"markup": "<div id='panel' class='panel'></div>",
"style": {
"left": "120px",
"top": "80px",
"position": "absolute"
},
"appendTo": "root",
"items": [
{
"tag": "h3",
"text": "Login Guide"
}
]
}attrs
attrs defines standard HTML attributes.
Example:
{
"attrs": {
"id": "panel",
"class": "panel",
"data-mode": "intro"
}
}What attrs Can Contain
Any normal HTML attribute is valid, for example:
idclasstypesrchreftitlealtroletabindexdata-*aria-*
If a node uses appendTo, the parent lookup should use normal HTML-style ids.
In other words, appendTo: "panel" refers to the element whose attrs.id is panel.
style
style defines inline CSS for that node.
Example:
{
"style": {
"position": "absolute",
"left": "120px",
"top": "80px",
"width": "280px",
"background": "#111827"
}
}What style Can Contain
Any normal inline-style-like CSS property may be used, for example:
positionlefttoprightbottomwidthheightpaddingmarginbackgroundbackgroundColorcolordisplayflexDirectiongapborderRadiustransformanimationtransitionzIndex
The runtime may normalize camelCase and kebab-case if desired, but the intent is that style behaves like inline CSS.
appendTo
appendTo defines the parent node by HTML id inside the cyghtml-managed overlay tree.
Example:
{
"appendTo": "panel"
}What appendTo Can Contain
appendTo may contain:
root- the
idof another cyghtml-created node
Examples:
appendTo: "root"appendTo: "panel"appendTo: "footer"
appendToClass
appendToClass defines the parent node by CSS class inside the cyghtml-managed overlay tree.
Example:
{
"appendToClass": "panel"
}What appendToClass Can Contain
appendToClass may contain:
- a class name without the leading
.
Examples:
appendToClass: "panel"appendToClass: "footer"
appendToClass Parent Rule
When appendToClass is used:
- the runtime should find the first matching rendered overlay element with that class
- that first matched overlay element becomes the parent
hostAppendTo
hostAppendTo defines the parent node by HTML id on the host page outside the cyghtml-managed tree.
Example:
{
"hostAppendTo": "app-shell"
}What hostAppendTo Can Contain
hostAppendTo may contain:
- the
idof an element already present on the host page
hostAppendToClass
hostAppendToClass defines the parent node by CSS class on the host page outside the cyghtml-managed tree.
Example:
{
"hostAppendToClass": "app-shell"
}What hostAppendToClass Can Contain
hostAppendToClass may contain:
- a class name without the leading
.
hostAppendToClass Parent Rule
When hostAppendToClass is used:
- the runtime should find the first matching host-page element with that class
- that first matched host-page element becomes the parent
Parent Resolution Rule
Parent lookup should follow this order:
appendToappendToClasshostAppendTohostAppendToClass
If none of them are present:
- for a top-level node in
html, the runtime may treatrootas the default parent - for a node inside
items, the current containing node becomes the default parent
This makes nested authoring simpler.
items
items is a list of child node definitions.
Important rule:
- every entry inside
itemsuses the same node schema as a top-levelhtmlnode - that means an item may also have its own
tag,markup,attrs,style,appendTo,appendToClass,hostAppendTo,hostAppendToClass,items,text, orevents
Example:
{
"tag": "div",
"attrs": {
"id": "panel"
},
"items": [
{
"tag": "div",
"attrs": {
"id": "header",
"class": "panel-header"
},
"items": [
{
"tag": "h3",
"text": "Login Guide"
}
]
}
]
}Parent Rule For items
If an item does not specify appendTo, its default parent is the current outer node.
So in this example:
{
"tag": "div",
"attrs": {
"id": "panel"
},
"items": [
{
"tag": "h3",
"text": "Login Guide"
}
]
}The h3 is automatically appended to panel.
If an item does specify appendTo, that explicit value wins.
text
text defines plain text content for a node.
Example:
{
"tag": "button",
"text": "Next"
}text Rules
textbehaves liketextContent- it works whether the node is top-level or inside
items - it is most natural with
tag - if
markupis used,textshould usually not be used at the same time because the intent becomes unclear
events
events defines DOM event bindings for a node.
Example:
{
"tag": "button",
"attrs": {
"id": "next"
},
"text": "Next",
"events": {
"click": "nextHandler"
}
}What events Can Contain
events is an object whose keys are DOM event names and whose values point to runtime handlers.
Examples of event keys:
clickmouseentermouseleaveinputchangefocusblur
How events Should Work
A handler name in JSON should be resolved against a runtime handler registry.
Example:
const cygh = new CygHtmlEngine()
cygh.handlers.nextHandler = () => {
console.log("next")
}If JSON contains:
{
"events": {
"click": "nextHandler"
}
}then the runtime should attach the DOM event so that clicking the element calls:
cygh.handlers["nextHandler"]Design Rule For events
For true JSON compatibility, the preferred form is a handler name string.
That means this is the recommended model:
{
"events": {
"click": "nextHandler"
}
}instead of embedding raw JavaScript functions inside JSON.
Variable Usage
Top-level vars values should be usable throughout the document.
Important rule:
- variable references should be resolved first, before normal render processing continues
That means the runtime should first build the vars map, then replace variable references in supported string values, and only then continue with node creation and final DOM commit.
The simplest first rule is:
varsvalues may be used incssvarsvalues may be used in inlinestylevarsvalues may be used inmarkupvarsvalues may also be compared in runtime conditions if cyghtml later exposes a condition system
Variable Usage In css
The most natural use in CSS is standard CSS variable syntax.
Example:
{
"vars": {
"--panel-x": "120px"
},
"css": {
".panel": {
"left": "var(--panel-x)"
}
}
}Variable Usage In style
Inline style should follow the same rule.
Example:
{
"style": {
"left": "var(--panel-x)",
"top": "var(--panel-y)"
}
}Variable Usage In markup
Because markup is raw HTML, variables may appear there as normal CSS variable usage inside inline styles or other supported string positions.
Example:
{
"markup": "<div id='panel' style='left:var(--panel-x); top:var(--panel-y);'></div>"
}Design rule:
- the runtime should resolve these variable references before final markup handling continues
- in practice, cyghtml should process variable substitution first, then parse and patch the resulting markup
Variable Usage In Plain Values
If cyghtml later needs to support value interpolation outside CSS, the cleanest direction is explicit placeholder syntax.
Example:
{
"text": "Current mode: {{mode}}"
}But for the initial version, CSS variable usage is the clearest and most natural model.
Variable Comparison
If cyghtml ever exposes conditional rendering or conditional patching later, variable comparison should use explicit condition expressions.
Examples:
mode === "intro"count > 3open === truedevice === "mobile" || device === "tablet"
That comparison system should remain optional. For the initial renderer-focused version of cyghtml, variables mainly exist to support rendering and styling.
Full Example
{
"vars": {
"--panel-x": "120px",
"--panel-y": "80px",
"--panel-bg": "#111827",
"--panel-color": "#ffffff"
},
"css": {
":root": {
"--shadow": "0 12px 30px rgba(0,0,0,0.24)"
},
".panel": {
"position": "absolute",
"left": "var(--panel-x)",
"top": "var(--panel-y)",
"background": "var(--panel-bg)",
"color": "var(--panel-color)",
"padding": "16px",
"border-radius": "14px",
"box-shadow": "var(--shadow)",
"width": "280px"
},
".panel button": {
"margin-top": "12px"
}
},
"html": [
{
"tag": "div",
"attrs": {
"id": "panel",
"class": "panel"
},
"appendTo": "root",
"items": [
{
"tag": "h3",
"attrs": {
"id": "title"
},
"text": "Login Guide"
},
{
"tag": "p",
"attrs": {
"id": "desc"
},
"text": "This overlay is rendered by cyghtml."
},
{
"tag": "button",
"attrs": {
"id": "next",
"type": "button"
},
"text": "Next"
}
]
}
]
}Processing Pipeline
This section describes how a cyghtml JSON document should move through the runtime, which files are responsible for each stage, and in what order the work happens.
The goal is to make the runtime behavior explicit before implementation begins.
End-To-End Flow
At a high level, the runtime flow is:
user JSON
-> CygHtmlEngine
-> OverlayRoot
-> CssRuntime
-> HtmlRuntime
-> pending parent resolution
-> final DOM commit to overlay rootA higher-level library such as cygtut may build the final JSON first, but once the JSON reaches cyghtml, the processing model should stay the same.
Input JSON Example
{
"vars": {
"--panel-x": "120px",
"--panel-y": "80px"
},
"css": {
".panel": {
"position": "absolute",
"left": "var(--panel-x)",
"top": "var(--panel-y)"
}
},
"html": [
{
"tag": "div",
"attrs": {
"id": "panel",
"class": "panel"
},
"appendTo": "root",
"items": [
{
"tag": "h3",
"text": "Login Guide"
}
]
}
]
}Step 1: JSON Enters CygHtmlEngine
File:
src/engine/CygHtmlEngine.ts
The engine is the main coordinator.
When code like this runs:
cygh.render(doc)or:
cygh.json = doc
cygh.render()The engine should:
- store the incoming JSON as the current document
- store or merge
varsinto runtime variable storage if needed - reset temporary render state for the new pass
- coordinate the rest of the pipeline
At this stage, the JSON is still only data. Nothing has been attached to the DOM yet.
Step 2: Create Or Reuse The Overlay Root
File:
src/renderer/OverlayRoot.ts
The overlay root is the top-most container created above the existing web page.
Responsibilities here:
- mount above
document.bodyby default when no explicit host is chosen - create the root if it does not already exist
- reuse it if it already exists
- prepare a safe rendering container for cyghtml nodes
Conceptually, the page becomes:
<body>
...existing page...
<div data-cyghtml-root="true"></div>
</body>At this point, cyghtml has a valid rendering destination, but no user nodes are attached yet.
Step 3: Resolve Top-Level vars
Files:
src/engine/CygHtmlEngine.tssrc/renderer/CssRuntime.tssrc/renderer/HtmlRuntime.ts
The runtime should resolve top-level vars values before normal CSS and HTML processing continues.
This is important because a variable may appear inside:
css- inline
style markup- text-like string values
- parent-related string values if the system later allows that
So the correct model is:
- read top-level
vars - build a runtime variable map
- replace variable references in all supported JSON string fields first
- only after that continue with CSS generation, node creation, style application, and parent linking
Example:
{
"vars": {
"--panel-x": "120px",
"--panel-bg": "#111827"
}
}If a later field contains values such as:
{
"style": {
"left": "var(--panel-x)"
},
"markup": "<div style='background:var(--panel-bg); left:var(--panel-x);'></div>"
}then those variable expressions should be resolved before the renderer applies style or parses the markup for final DOM creation.
This matters because early variable resolution makes later stages predictable. For example:
- storing processed node state becomes simpler
- markup parsing becomes more stable
- style patching becomes more stable
- parent linking or delayed node attachment can work with already-resolved values
Step 4: Apply Top-Level css
File:
src/renderer/CssRuntime.ts
The runtime then processes the top-level css section.
Responsibilities:
- read selector keys
- read declaration objects
- generate CSS text or CSSOM rules
- attach the generated rules to the runtime style container
Example input:
{
"css": {
".panel": {
"position": "absolute",
"left": "var(--panel-x)",
"top": "var(--panel-y)"
}
}
}Important note:
The runtime may keep CSS application scoped to its own overlay output so host-page styles and overlay styles remain easier to reason about.
Step 5: Read Top-Level html
Files:
src/engine/CygHtmlEngine.tssrc/renderer/HtmlRuntime.ts
The engine now walks the top-level html array.
Each item in the array is treated as a node definition. The engine should pass each node definition into the HTML runtime.
At this stage, the runtime should not assume that parent nodes already exist. This is important because parent references may be forward-declared.
Step 6: Create DOM Nodes From tag Or markup
File:
src/renderer/HtmlRuntime.ts
For each HTML node definition, the runtime should create a real DOM node.
There are two main paths.
Path A: tag
If the node uses tag:
- call the DOM equivalent of
document.createElement(tag) - create exactly one real element node
- keep a temporary registry entry for that node
Example:
{
"tag": "button",
"attrs": {
"id": "next"
},
"text": "Next"
}Path B: markup
If the node uses markup:
- parse the raw markup string
- require exactly one outer root element
- take that outer root as the node's real DOM element
- keep a temporary registry entry for that node
Example:
{
"markup": "<div id='panel' class='panel'></div>"
}At the end of this step, the runtime has created real DOM elements, but they may not yet be attached to their final parents.
Step 7: Apply Node-Level attrs
File:
src/renderer/HtmlRuntime.ts
Once a node exists, attrs should be applied.
Responsibilities:
- set standard HTML attributes
- preserve authoring ids and classes exactly as written unless the runtime explicitly documents another transform
- keep normal HTML semantics predictable
Example input:
{
"attrs": {
"id": "panel",
"class": "panel active",
"data-mode": "intro"
}
}The expected result is that those attributes are applied directly to the rendered node.
Step 8: Apply Node-Level style
File:
src/renderer/HtmlRuntime.ts
After attributes, the runtime should apply inline style.
Responsibilities:
- map JSON style keys to DOM style assignments
- allow normal CSS-like values
- if the node came from
markup, apply style to the outer root element - if markup already contains inline style, apply the outer JSON
stylelast as a final patch
Example:
{
"style": {
"position": "absolute",
"left": "120px",
"top": "80px"
}
}Step 9: Apply text
File:
src/renderer/HtmlRuntime.ts
If a node defines text, the runtime should apply it like textContent.
Example:
{
"tag": "h3",
"text": "Login Guide"
}If a node uses markup, text should usually be avoided because the meaning becomes ambiguous.
That should be treated as an authoring rule.
Step 10: Walk items
Files:
src/engine/CygHtmlEngine.tssrc/renderer/HtmlRuntime.ts
If a node contains items, each item is processed using the exact same node schema.
That means each item may itself contain:
tagmarkupattrsstyleappendToappendToClasshostAppendTohostAppendToClassitemstextevents
Processing rule:
- create the child node
- if no explicit parent is set, use the current containing node as the default parent
- if an explicit parent is set, store that relation for later resolution
Step 11: Store Parent Links Instead Of Attaching Immediately
File:
src/engine/CygHtmlEngine.ts
This is one of the most important parts of the design.
cyghtml should not require perfect authoring order. That means the engine should be allowed to create nodes first and connect them later.
So for every node, the engine should store:
- the real DOM element
- the authoring id if present
- the desired parent target
- whether that parent target is by id, by class, or by root
A simple internal registry may look like:
node registry:
- panel -> HTMLElement
- title -> HTMLElement
- next -> HTMLElement
pending parent links:
- next -> panel
- title -> panelThis allows forward references such as:
{
"html": [
{
"tag": "button",
"attrs": {
"id": "next"
},
"appendTo": "panel",
"text": "Next"
},
{
"tag": "div",
"attrs": {
"id": "panel"
},
"appendTo": "root"
}
]
}Even though panel appears later, the final connection can still succeed.
Step 12: Resolve appendTo
File:
src/engine/CygHtmlEngine.ts
When resolving parent links:
- if
appendToisroot, attach the node to the overlay root - if
appendTois an id, find the registered node with that authoring id - attach the child to that resolved parent
appendTo should take priority over appendToClass.
Step 13: Resolve appendToClass
Files:
src/engine/CygHtmlEngine.tssrc/renderer/HtmlRuntime.ts
If appendTo is missing and appendToClass is present:
- find the first matching class target in the runtime-managed rendered set
- use that first matched element as the parent
- attach the child to it
This rule should stay simple and explicit. The first match wins.
Step 14: Final DOM Commit
Files:
src/engine/CygHtmlEngine.tssrc/renderer/HtmlRuntime.ts
Once all nodes are created and all parent links are resolved, the runtime commits the result into the overlay root.
That means:
- top-level root-attached nodes are appended to the overlay root
- child nodes are appended to their resolved parents
- the final overlay DOM becomes visible above the page
At this point, the current JSON document has fully become real overlay DOM.
Step 15: Re-Render On New JSON
File:
src/engine/CygHtmlEngine.ts
When the outside system provides a new JSON document:
- the engine stores it as the new current document
- previous runtime render state is cleared or replaced
- the pipeline runs again from the beginning
This means cyghtml can stay simple:
- it stores the current JSON
- when JSON changes, it renders again
A higher-level library such as cygtut may decide when and why the JSON changes.
cyghtml only needs to render the new result.
File-By-File Responsibility In The Pipeline
src/engine/CygHtmlEngine.ts
Responsible for:
- storing current JSON
- storing runtime vars
- starting render passes
- building and keeping the node registry
- keeping unresolved parent links
- resolving parent links in the correct order
- clearing and destroying runtime state
src/renderer/OverlayRoot.ts
Responsible for:
- creating the top-most overlay container
- mounting to
bodyby default - exposing the root element back to the engine
- cleaning up the overlay root when needed
src/renderer/CssRuntime.ts
Responsible for:
- applying top-level
vars - applying top-level
css - generating runtime stylesheets
- attaching generated runtime CSS in a predictable overlay-owned way
src/renderer/HtmlRuntime.ts
Responsible for:
- creating nodes from
tag - creating nodes from
markup - applying attrs
- applying style
- applying text
- returning finished DOM nodes back to the engine
src/types/schema.ts
Responsible for:
- defining the JSON schema
- defining top-level
vars,css, andhtml - defining the node shape and allowed keys
src/index.ts
Responsible for:
- exporting
CygHtmlEngine - exporting public document and node types
Why This Pipeline Matters
This processing order makes cyghtml predictable.
It allows:
- normal HTML/CSS-like authoring
- forward parent references
- parent resolution at the end
- markup or tag based authoring
- clean separation from higher-level libraries such as cygtut
The important design boundary is:
- cyghtml turns final JSON into overlay DOM
- cygtut decides what JSON should exist in the first place
Runtime Class
The main runtime class is planned as:
const cygh = new CygHtmlEngine(options?)cyghtml is a library package, so its runtime API should be documented clearly.
Runtime Properties
A CygHtmlEngine instance should expose these public properties.
json
Current cyghtml document stored on the engine.
Example:
cygh.json = doc
cygh.render()Purpose:
- stores the current source document
- lets the engine re-render from the current document without requiring the caller to pass it every time
vars
Runtime variable storage.
Example:
cygh.vars.abc = 1
console.log(cygh.vars.abc)Purpose:
- keep runtime values that outside systems want to read or write
- provide a simple shared value bag for the current render runtime
root
Reference to the actual overlay root element created by the engine.
Example:
console.log(cygh.root)Purpose:
- gives direct access to the mounted overlay root
- is read-oriented runtime state, not the preferred way to choose the mount target
- useful for debugging or advanced integrations
interactive
Whether the overlay root should allow pointer interaction at the root level.
Example:
const cygh = new CygHtmlEngine({ interactive: true })
console.log(cygh.interactive)
``
Purpose:
- `false` keeps the overlay root non-blocking by default
- `true` allows the overlay root itself to behave like an interactive app container
- useful for app-like demos, dashboards, or full overlay UIs
### `mounted`
Boolean-like mounted state.
Example:
```ts
console.log(cygh.mounted)Purpose:
- tells whether the overlay root is currently attached and active
handlers
Runtime handler registry.
Example:
cygh.handlers.nextHandler = () => {
console.log("next")
}Purpose:
- stores named runtime functions for
eventsbindings - lets JSON stay data-oriented while runtime code provides actual functions
Runtime Methods
A CygHtmlEngine instance should expose these public methods.
render(document?)
Render a full cyghtml document. If a document is passed, store it first as the current json and then render it. If not, render the currently stored json.
cygh.render(doc)
cygh.render()Purpose:
- store the current source document when provided
- apply top-level
vars - apply top-level
css - build the overlay DOM from top-level
html
destroy()
Destroy the overlay runtime.
cygh.destroy()Purpose:
- remove the overlay root
- clear style tags and rendered nodes
- release internal references
clear()
Clear rendered overlay content without destroying the instance itself.
cygh.clear()Purpose:
- empty current overlay nodes
- keep the engine instance reusable
setVar(name, value)
Write a runtime variable.
cygh.setVar("abc", 1)Purpose:
- explicit variable write API
- preferred over assigning random properties directly on the engine instance
getVar(name)
Read a runtime variable.
console.log(cygh.getVar("abc"))Purpose:
- explicit variable read API
setVars(record)
Write many runtime variables at once.
cygh.setVars({ abc: 1, mode: "intro" })Purpose:
- convenient multi-variable update API
getSnapshot()
Return a runtime snapshot.
console.log(cygh.getSnapshot())A snapshot may contain:
- current
vars - the last rendered document
- root or mount state
- internal node registry state if the public API chooses to expose it
Constructor options
Supported options:
document?: Documentinteractive?: boolean
Example:
const passive = new CygHtmlEngine()
const interactive = new CygHtmlEngine({ interactive: true })
``
Default behavior:
- `interactive` defaults to `false`
- when `interactive` is `false`, the root uses `pointer-events: none`
- when `interactive` is `true`, the root uses `pointer-events: auto`
### `mount(target?)`
Mount the overlay root.
```ts
cygh.mount()
cygh.mount(document.body)Purpose:
- create and attach the overlay root when needed
- use
document.bodyas the default host when no target is provided - support custom host mounting in advanced scenarios
mountById(id)
Mount the overlay root to the first host element found by id.
cygh.mountById("app")mountByClass(className)
Mount the overlay root to the first host element found by class name.
cygh.mountByClass("app-shell")mountByTag(tagName)
Mount the overlay root to the first host element found by tag name.
cygh.mountByTag("main")unmount()
Detach the overlay root without fully destroying the engine instance.
cygh.unmount()Purpose:
- temporarily remove the overlay from the page
- preserve engine state if desired
removeNode(id)
Remove a rendered node by authoring id.
cygh.removeNode("panel")Purpose:
- remove a specific rendered node
- remove the matching stored JSON subtree when available
- keep future
render()calls from restoring the removed node unless new JSON is provided
updateNode(id, patch)
Partially update one existing authoring node.
cygh.updateNode("panel", {
style: { left: "240px" }
})Purpose:
- mutate one stored JSON node
- trigger a stable re-render from the updated state
replaceNode(id, node)
Replace one existing authoring node.
cygh.replaceNode("title", {
tag: "h2",
attrs: { id: "title" },
text: "Updated title"
})appendNode(parentId, node)
Append a child node to a managed parent or to root.
cygh.appendNode("panel", {
tag: "button",
attrs: { id: "next", type: "button" },
text: "Next"
})prependNode(parentId, node)
Insert a child node at the beginning of a managed parent or root.
cygh.prependNode("root", {
tag: "div",
attrs: { id: "banner" },
text: "Pinned first"
})Recommended Public Usage
The normal usage style should look like this:
const cygh = new CygHtmlEngine()
cygh.handlers.nextHandler = () => {
console.log("next")
}
cygh.setVar("abc", 1)
console.log(cygh.getVar("abc"))
cygh.json = {
vars: {
"--panel-x": "120px"
},
css: {
".panel": {
"position": "absolute",
"left": "var(--panel-x)"
}
},
html: [
{
"tag": "div",
"attrs": {
"id": "panel",
"class": "panel"
},
"appendTo": "root",
"text": "Hello"
}
]
}
cygh.render()Render Modes
cyghtml should support two runtime usage styles.
1. Full render
This is the current default model.
cygh.render(doc)or:
cygh.json = doc
cygh.render()Meaning:
- store the current JSON document
- resolve
vars - clear current overlay children
- rebuild CSS and DOM from the stored document
This is the safest and simplest mode.
It is the best fit for higher-level runtimes such as cygtut, where the final overlay document may be fully recompiled whenever tutorial state changes.
2. Incremental update
This is the current node-level update model.
It should allow users to keep the current rendered tree and update only part of it instead of always doing a full rebuild.
Examples:
cygh.updateNode("panel", {
style: { left: "240px" }
})
cygh.updateNode("title", {
text: "Updated title"
})
cygh.replaceNode("title", {
tag: "h2",
attrs: { id: "title" },
text: "Updated title"
})
cygh.appendNode("panel", {
tag: "button",
attrs: { id: "next" },
text: "Next"
})Current goals:
- keep the existing overlay DOM when only a small part changes
- update both the live DOM and the stored
json - let later
render()calls continue from the updated stored state - fall back to subtree replacement when a fine-grained patch is too complex
Why both modes matter
Both models are useful.
Full render is better when:
- a higher-level engine recompiles the whole overlay document
- layout or structure changes broadly
- correctness matters more than patch efficiency
Incremental update is better when:
- a text label changes
- a class or style needs to be toggled
- one node is appended or removed
- a small subtree changes without affecting the rest of the overlay
Incremental Update API
The current runtime already supports:
render(document?)updateNode(id, patch)replaceNode(id, node)appendNode(parentId, node)prependNode(parentId, node)removeNode(id)clear()destroy()
The current runtime now supports node-level update methods by mutating the stored json and then re-running render() for a stable rebuild.
updateNode(id, patch)
Partially update one existing authoring node and then re-render from the updated stored json.
Planned patch surface:
attrsstyletextmarkupeventsitems
Important rule:
- this updates the stored JSON node first
- the current runtime then calls
render()so the live DOM is rebuilt from the updated state
replaceNode(id, node)
Replace a rendered node with a new node definition, then re-render from the updated stored json.
This is useful when a node changes too much for a small patch to stay simple.
appendNode(parentId, node)
Append a new child node under an existing managed parent node, then re-render from the updated stored json.
prependNode(parentId, node)
Insert a new child node at the beginning of an existing managed parent node, then re-render from the updated stored json.
removeNode(id)
removeNode(id) now removes the matching stored JSON subtree when possible and then re-renders.
If the node exists only in the current live DOM and not in stored json, it falls back to removing the live DOM subtree directly.
Internal Rule For Incremental Updates
Because cyghtml now supports node-level updates, the runtime should not treat DOM and JSON as separate truths.
The rule should be:
- stored
jsonis the canonical render state - DOM is the live projection of that state
- incremental updates must mutate stored
jsonand then keep the DOM in sync through re-rendering or targeted subtree rebuilding
That means a future patch flow should look like this:
update request
-> locate node in stored json
-> patch stored json
-> patch live DOM or rebuild the affected subtree
-> keep the rest of the tree intactThis keeps full render and incremental update compatible with each other.
Design Rule For Runtime State
The engine instance itself should not be used like this:
cygh.abc = 1Even though JavaScript technically allows arbitrary properties, cyghtml should treat runtime values as:
cygh.vars.abc = 1cygh.setVar("abc", 1)cygh.getVar("abc")
This keeps the public API explicit and easier to maintain.
Why This Model
This model keeps cyghtml close to normal web authoring.
attrs.idis a normal HTML idappendToworks like a parent referencetagbehaves likedocument.createElement(tag)markupbehaves like raw HTML inputstylebehaves like inline CSScssbehaves like stylesheet rulesvarsbehaves like injected CSS variables or external values
The result is not a tutorial DSL. It is a low-level HTML/CSS overlay description format.
Folder Structure
The planned cyghtml project structure is:
cyghtml/
README.md
package.json
src/
engine/
CygHtmlEngine.ts
renderer/
OverlayRoot.ts
CssRuntime.ts
HtmlRuntime.ts
types/
schema.ts
index.tsThis is not just a recommendation. This is the intended implementation structure for cyghtml.
File Roles
src/engine/CygHtmlEngine.ts
Main runtime coordinator.
Responsibilities:
- accept a full cyghtml document
- keep a temporary node registry
- resolve pending parent links
- call renderer helpers
- expose lifecycle methods such as
render,removeNode,clear, anddestroy
src/renderer/OverlayRoot.ts
Responsible for overlay mounting.
Responsibilities:
- create and own the top-most overlay root
- mount above
document.bodyby default - clear or destroy the overlay when needed
src/renderer/CssRuntime.ts
Responsible for top-level CSS and variable application.
Responsibilities:
- apply
vars - generate style tags from
css - keep selector rules attached to the overlay document
src/renderer/HtmlRuntime.ts
Responsible for turning node definitions into actual DOM nodes.
Responsibilities:
- create nodes from
tag - create nodes from
markup - apply
attrs - apply
style - apply
text - attach nodes to resolved parents
src/types/schema.ts
Type definitions for the cyghtml document.
Responsibilities:
- define top-level
vars,css,html - define node schema
- define
appendTo,appendToClass,hostAppendTo,hostAppendToClass,items,markup,tag,attrs,style,text, andevents
src/index.ts
Public package entry.
Responsibilities:
- export the engine
- export public types
Detailed Usage Examples
This section shows how cyghtml is meant to be used in practice.
1. Basic Overlay Render
import { CygHtmlEngine } from "cyghtml";
const cygh = new CygHtmlEngine({ interactive: true });
cygh.render({
vars: {
"--panel-x": "120px",
"--panel-y": "80px",
"--panel-bg": "#111827"
},
css: {
".panel": {
position: "absolute",
left: "var(--panel-x)",
top: "var(--panel-y)",
width: "280px",
padding: "16px",
borderRadius: "14px",
background: "var(--panel-bg)",
color: "#ffffff"
}
},
html: [
{
tag: "div",
attrs: {
id: "panel",
class: "panel"
},
appendTo: "root",
items: [
{
tag: "h3",
text: "Hello from cyghtml"
},
{
tag: "p",
text: "This node is rendered into the overlay root."
}
]
}
]
});What this does:
- creates or reuses the overlay root
- applies
varsto the runtime - applies the stylesheet from
css - creates the
panelnode - appends it to
root - creates child items and appends them to
panel
2. Rendering With Stored json
import { CygHtmlEngine } from "cyghtml";
const cygh = new CygHtmlEngine({ interactive: true });
cygh.json = {
vars: {
"--panel-x": "24px",
"--panel-y": "24px"
},
css: {
".panel": {
position: "absolute",
left: "var(--panel-x)",
top: "var(--panel-y)",
padding: "12px",
background: "#111827",
color: "#ffffff"
}
},
html: [
{
tag: "div",
attrs: { id: "panel", class: "panel" },
appendTo: "root",
text: "Render using stored json"
}
]
};
cygh.render();This is useful when another runtime updates cygh.json first and only then asks cyghtml to render.
3. Mounting To A Custom Host
By default, cyghtml mounts above document.body.
By default, the root also uses pointer-events: none, which is ideal for guide overlays that should not block the host page.
If the overlay itself is meant to be the interactive UI, use interactive: true.
You can also choose another host.
const cygh = new CygHtmlEngine({ interactive: true });
cygh.mountById("app");
cygh.render({
html: [
{
tag: "div",
attrs: { id: "panel" },
appendTo: "root",
text: "Mounted inside #app instead of body"
}
]
});You can also use:
cygh.mountByClass("app-shell");
cygh.mountByTag("main");
cygh.mount(document.body);4. Events And Handlers
import { CygHtmlEngine } from "cyghtml";
const cygh = new CygHtmlEngine({ interactive: true });
cygh.handlers.nextHandler = () => {
console.log("next clicked");
};
cygh.render({
html: [
{
tag: "button",
attrs: {
id: "next",
type: "button"
},
style: {
position: "absolute",
left: "24px",
top: "24px",
pointerEvents: "auto"
},
appendTo: "root",
text: "Next",
events: {
click: "nextHandler"
}
}
]
});How this works:
- the JSON keeps only the handler name
- the runtime resolves that name from
cygh.handlers - clicking the button calls
cygh.handlers.nextHandler
5. Using markup With Outer Patches
const cygh = new CygHtmlEngine({ interactive: true });
cygh.render({
html: [
{
markup: "<div id='panel' class='panel' style='padding:12px;'><strong>Markup root</strong></div>",
style: {
position: "absolute",
left: "120px",
top: "80px",
background: "#111827",
color: "#ffffff"
},
appendTo: "root",
items: [
{
tag: "button",
attrs: {
id: "next",
type: "button"
},
style: {
marginTop: "12px",
pointerEvents: "auto"
},
text: "Next"
}
]
}
]
});Important rule:
markupmust create exactly one outer root element- outer
style,attrs, anditemsapply to that outer root element - inline markup styles are allowed
- outer
styleacts like a final patch on the root element
6. Nested items
const cygh = new CygHtmlEngine({ interactive: true });
cygh.render({
html: [
{
tag: "div",
attrs: {
id: "panel",
class: "panel"
},
style: {
position: "absolute",
left: "120px",
top: "80px",
width: "280px"
},
appendTo: "root",
items: [
{
tag: "div",
attrs: {
id: "header",
class: "panel-header"
},
items: [
{
tag: "h3",
text: "Nested header"
}
]
},
{
tag: "div",
attrs: {
id: "footer"
},
items: [
{
tag: "button",
attrs: {
id: "next",
type: "button"
},
text: "Next"
}
]
}
]
}
]
});Default item rule:
- if an item does not define a parent override, its parent becomes the current containing node
- that means
headerandfooterbecome children ofpanel - and the nested
h3becomes a child ofheader
7. Forward Parent References
cyghtml allows delayed parent resolution. That means authoring order does not have to be perfect.
const cygh = new CygHtmlEngine({ interactive: true });
cygh.render({
html: [
{
tag: "button",
attrs: {
id: "next",
type: "button"
},
appendTo: "panel",
text: "Next"
},
{
tag: "div",
attrs: {
id: "panel"
},
appendTo: "root"
}
]
});Even though panel appears later, the runtime can still:
- create both nodes first
- store parent links temporarily
- resolve the final parent relationship at the end
8. Appending To Existing Host Page DOM
Sometimes a node should attach to an element that already exists on the page outside the cyghtml-managed overlay tree.
By host id
const cygh = new CygHtmlEngine({ interactive: true });
cygh.render({
html: [
{
tag: "div",
attrs: { id: "inline-panel" },
hostAppendTo: "app-shell",
text: "Attached to existing host node by id"
}
]
});By host class
const cygh = new CygHtmlEngine({ interactive: true });
cygh.render({
html: [
{
tag: "div",
attrs: { id: "inline-panel" },
hostAppendToClass: "app-shell",
text: "Attached to first existing host node by class"
}
]
});Host parent lookup rules:
hostAppendTolooks up an existing host-page element byidhostAppendToClasslooks up the first matching host-page class element- these host lookups should stay outside the cyghtml-managed overlay tree
9. Updating Variables And Re-Rendering
const cygh = new CygHtmlEngine({ interactive: true });
cygh.render({
vars: {
"--panel-x": "24px"
},
css: {
".panel": {
position: "absolute",
left: "var(--panel-x)",
top: "24px"
}
},
html: [
{
tag: "div",
attrs: { id: "panel", class: "panel" },
appendTo: "root",
text: "Move me by changing vars"
}
]
});
cygh.setVar("--panel-x", "200px");
cygh.render();This works because:
- the engine keeps
json - the engine keeps runtime
vars render()resolvesvarsfirst- the next render re-applies CSS and DOM from the current state
10. Cleanup
const cygh = new CygHtmlEngine({ interactive: true });
cygh.render(doc);
cygh.removeNode("panel");
cygh.clear();
cygh.unmount();
cygh.destroy();Use these when:
removeNode(id)removes one rendered subtree by authoring idclear()removes current overlay children but keeps the instance aliveunmount()removes the overlay root from the pagedestroy()fully tears down runtime state
Summary
cyghtml is a low-level overlay renderer.
It should:
- accept
vars,css, andhtml - stay close to HTML and CSS semantics
- support both
tagandmarkup - support nested
items - allow delayed parent resolution through
appendTo,appendToClass,hostAppendTo, andhostAppendToClass - render final DOM into a top-most overlay root
And it should deliberately stop there.
Higher-level meaning such as tutorial flow belongs in cygtut.
