A minimal, zero-dependency library for building fully-styled HTML in TypeScript/JavaScript.
Features
- Zero dependencies.
- Arbitrary CSS with content-based classes and CSS chunk deduplication.
- Reusable "Component"-pattern for composing common UIs.
Quick Look
import { css, Document, node } from "@lonnycorp/htmlforge"
const html = new Document()
html.attribute("lang", "en-GB")
html.head.child(
new node.Element("title").child(
new node.Text("Acme Title")
)
)
html.body.child(
new node.Element("div")
.css(cls => css.css`
.${cls} { width: 100%; background-color: blue; }
.${cls}:hover { background-color: red; }
`)
.child(new node.Text("Hello world"))
)
const validHTML = html.toString()Installation
Install the package from npm:
npm install @lonnycorp/htmlforgeUsage
HTML structure
An HTMLForge Document instance is a tree of nodes. Nodes come in a few flavors:
node.Element: represents tags (e.g.,<div>,<span>), can hold attributes and CSS chunks, and can nest any other node as a child.node.Text: holds plain text content that will be HTML-escaped.node.Raw: holds raw HTML without escaping.node.Fragment: groups a collection of child nodes without introducing a wrapping element.
Creating an HTML document
Create a new HTML document using new Document(), set attributes on the root <html> element via html.attribute, and work directly with html.head and html.body to populate content. The constructor accepts optional parameters:
displaySignature(defaulttrue): whether to include the<!-- Generated by HTMLForge -->signature comment.
import { css, Document, node } from "@lonnycorp/htmlforge"
const html = new Document({ displaySignature: false })
.attribute("lang", "en")
.attribute("data-theme", "dark")
html.body
.css(cls => css.css`.${cls} { margin: auto; }`)
.child(new node.Text("Hello world"))node.Element nodes
node.Element supports:
attribute(name, value)for HTML attributescss(build)for complete CSS rules, with a callback receiving a class placeholderchild(node)to nest children nodes
These calls are chainable to keep element construction compact.
Each .css() callback runs once and returns a css.Chunk from the css.css tagged template. Interpolate the placeholder as .${cls} in selectors or ${cls}-enter in animation names. The chunk hash becomes its class name. Multiple calls attach multiple classes, and identical templates and interpolation values are emitted once per document.
CSS is emitted verbatim, so media queries, keyframes, and global rules can be placed at the stylesheet level. String and number interpolations are inserted without escaping.
Chunks can also be reused with .css(() => sharedChunk). When rules have equal specificity, their order in the document stylesheet determines precedence; chaining order does not override CSS cascade rules.
import { css, node } from "@lonnycorp/htmlforge"
const card = new node.Element("section")
.attribute("aria-label", "profile card")
.css(cls => css.css`.${cls} { border: 1px solid #ccc; }`)
.child(
new node.Element("h2").child(new node.Text("Ada Lovelace"))
)
.child(
new node.Element("p")
.css(cls => css.css`.${cls} { color: #555; }`)
.child(new node.Text("First computer programmer."))
)node.Fragment nodes
node.Fragment groups child nodes without adding a wrapper element. It only supports child (also chainable).
import { node } from "@lonnycorp/htmlforge"
const listItems = new node.Fragment()
.child(new node.Element("li").child(new node.Text("One")))
.child(new node.Element("li").child(new node.Text("Two")))
.child(new node.Element("li").child(new node.Text("Three")))Text and Raw nodes
node.Textholds HTML-escaped text content (no additional methods).node.Rawinjects raw HTML as-is (no additional methods).
import { node } from "@lonnycorp/htmlforge"
const safeText = new node.Text("<em>Escaped</em> output")
const rawHtml = new node.Raw("<em>Unescaped</em> output")Define your own nodes
Implement the node.Buildable interface to build reusable components. Compose a private node.Element (style/shape it however you like) and proxy its build() method. Anything that implements node.Buildable can be passed to child on node.Element or node.Fragment.
import { css, node } from "@lonnycorp/htmlforge"
class Alert implements node.Buildable {
private readonly el = new node.Element("div")
.attribute("role", "alert")
.css(cls => css.css`.${cls} { padding: 12px 16px; background-color: #fffae6; }`)
constructor(message: string) {
this.el.child(new node.Text(message))
}
// Optional: expose child to let callers inject arbitrary child nodes
child(child: node.Buildable) {
this.el.child(child)
return this
}
build() {
return this.el.build()
}
}