HTMLForge

A minimal, zero-dependency library for building fully-styled HTML in TypeScript/JavaScript.

Features

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

Usage

HTML structure

An HTMLForge Document instance is a tree of nodes. Nodes come in a few flavors:

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:

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:

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

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