HTML/SVG/XML serialization of nested data structures, iterables & closures
npm install @thi.ng/hiccup
!npm downloads

> [!NOTE]
> This is one of 214 standalone projects, maintained as part
> of the @thi.ng/umbrella monorepo
> and anti-framework.
>
> 🚀 Please help me to work full-time on these projects by sponsoring me on
> GitHub. Thank you! ❤️
- About
- Features
- Use cases
- No special sauce needed (or wanted)
- What is Hiccup?
- Status
- Support packages
- Related packages
- Blog posts
- Installation
- Dependencies
- Usage examples
- API
- Tags with Zencoding expansion
- Attributes
- Simple components
- User context injection
- SVG generation, generators & lazy composition
- Data-driven component composition
- Stateful component
- Component objects
- Behavior control attributes
- Comments
- Inlined/embedded markup
- XML / DTD processing instructions
- API
- serialize()
- Authors
- License
HTML/SVG/XML serialization of nested data structures, iterables & closures.
Inspired by Hiccup and
Reagent for Clojure/ClojureScript, this
package provides key infrastructure for a number of other related libraries.
Forget all the custom toy DSLs for templating and instead use the full power of
modern JavaScript to directly define fully data-driven, purely functional and
easily composable components for static serialization to HTML & friends.
This library is suitable for any SGML-style (HTML/XML/SVG/RSS/Atom etc.)
serialization, including static website/asset generation, server side rendering
etc. For interactive use cases, please see companion packages
@thi.ng/rdom
(or the older, now unmaintained
@thi.ng/hdom)
and their various support packages.
- Only uses JS arrays, plain objects, functions, ES6 iterables / iterators / generators
- Eager & lazy component composition using embedded functions / closures
- Support for self-closing tags (incl. validation), boolean attributes
- Arbitrary user context object injection for embedded component functions
- Dynamically derived attribute value generation via function values
- CSS formatting of style attribute objects
- Optional HTML/XML entity encoding
- Support for comments and XML/DTD processing instructions
- Branch-local behavior control attributes to customize serialization
- Small (1.9KB minified) & fast
(*) Lazy composition here means that functions are only executed at
serialization time. Examples below...
- Serverside rendering
- Static site, feed generation
- .innerHTML body generation
- SVG asset creation
- Shape trees for declarative canvas API drawing
- Generic intermediate representation format for many other use cases...
Using only vanilla language features simplifies the development, removes need
for extra tooling, improves composability, reusability, transformation and
testing of components. No custom template parser (a la JSX or Handlebars etc.)
is required and you're only restricted by the expressiveness of the language /
environment, not by your template engine.
Components can be defined as simple arrays and/or functions returning arrays or
can be dynamically generated or loaded via JSON...
For many years, Hiccup has been the
de-facto standard to encode HTML/XML datastructures in Clojure (and many years
before that, the overall idea was introduced in Scheme by Oleg Kiselyov and
Kirill Lisovsky in
1999).
This library brings & extends this convention into ES6. A valid Hiccup tree is
any flat (though, usually nested) array of the following possible structures.
Any functions embedded in the tree are expected to return values of the same
structure. Please see examples & API further
explanations...
``ts`
["tag", ...]
["tag#id.class1.class2", ...]
["tag", {other: "attrib", ...}, ...]
["tag", {...}, "body", 23, function, [...]]
[function, arg1, arg2, ...]
[{render: (ctx, ...args) => [...]}, args...]
iterable
STABLE - used in production
Search or submit any issues for this package
- @thi.ng/hiccup-canvas - Hiccup shape tree renderer for vanilla Canvas 2D contexts
- @thi.ng/hiccup-carbon-icons - Full set of IBM's Carbon icons in hiccup format
- @thi.ng/hiccup-css - CSS from nested JS data structures
- @thi.ng/hiccup-html - 100+ type-checked HTML5 element functions for @thi.ng/hiccup related infrastructure
- @thi.ng/hiccup-html-parse - Well-formed HTML parsing and customizable transformation to nested JS arrays in @thi.ng/hiccup format
- @thi.ng/hiccup-markdown - Markdown parser & serializer from/to Hiccup format
- @thi.ng/hiccup-svg - SVG element functions for @thi.ng/hiccup & related tooling
- @thi.ng/axidraw - Minimal AxiDraw plotter/drawing machine controller for Node.js
- @thi.ng/geom - Functional, polymorphic API for 2D geometry types & SVG generation
- @thi.ng/geom-axidraw - Conversion and preparation of thi.ng/geom shapes & shape groups to/from AxiDraw pen plotter draw commands
- @thi.ng/hdom - Lightweight vanilla ES6 UI component trees with customizable branch-local behaviors
- @thi.ng/hdom-canvas - @thi.ng/hdom component wrapper for declarative canvas scenegraphs
- @thi.ng/hdom-components - Raw, skinnable UI & SVG components for @thi.ng/hdom
- @thi.ng/rdom - Lightweight, reactive, VDOM-less UI/DOM components with async lifecycle and @thi.ng/hiccup compatible
- @thi.ng/rdom-canvas - @thi.ng/rdom component wrapper for @thi.ng/hiccup-canvas and declarative canvas drawing
- @thi.ng/rdom-components - Collection of unstyled, customizable components for @thi.ng/rdom
- @thi.ng/transducers - Collection of ~170 lightweight, composable transducers, reducers, generators, iterators for functional data transformations
- @thi.ng/zipper - Functional tree editing, manipulation & navigation
- How to UI in 2018
- Of umbrellas, transducers, reactive streams & mushrooms (Pt.1)
`bash`
yarn add @thi.ng/hiccup
ESM import:
`ts`
import * as h from "@thi.ng/hiccup";
Browser ESM import:
`html`
For Node.js REPL:
`js`
const h = await import("@thi.ng/hiccup");
Package sizes (brotli'd, pre-treeshake): ESM: 2.24 KB
- @thi.ng/api
- @thi.ng/checks
- @thi.ng/errors
- @thi.ng/strings
Note: @thi.ng/api is in _most_ cases a type-only import (not used at runtime)
11 projects in this repo's
/examples
directory are using this package:
| Screenshot | Description | Live demo | Source |
|:-------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------|:------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------|
|
| Heatmap visualization of this mono-repo's commits | | Source |
|
| Filterable commit log UI w/ minimal server to provide commit history | Demo | Source |
|
| Applying thi.ng/hdiff to generate static HTML diff output | Demo | Source |
|
| Various hdom-canvas shape drawing examples & SVG conversion / export | Demo | Source |
|
| Generating pure CSS image transitions | Demo | Source |
| | Hiccup / hdom DOM hydration example | Demo | Source |
|
| Markdown to Hiccup to HTML parser / transformer | Demo | Source |
|
| CLI util to visualize umbrella pkg stats | | Source |
|
| Generate SVG using pointfree DSL | | Source |
|
| Basic usage of the declarative rdom-forms generator | Demo | Source |
|
| Interactive grid generator, SVG generation & export, undo/redo support | Demo | Source |
Tag names support
Emmet/Zencoding
style ID & class attribute expansion:
`ts
import { serialize } from "@thi.ng/hiccup";
serialize(
["div#yo.hello.world", "Look ma, ", ["strong", "no magic!"]]
);
`
`html`Look ma, no magic!
Arbitrary attributes can be supplied via an optional 2nd array element.
style attributes can be given as CSS string or as an object. Boolean
attributes are serialized in HTML5 syntax (i.e. present or not, but no
values).
If the 2nd array element is not a plain object, it's treated as normal
child node (see previous example).
`ts
import { serialize } from "@thi.ng/hiccup";
serialize(
["div.notice",
{
selected: true,
style: {
background: "#ff0",
border: "3px solid black"
}
},
"WARNING"]
);
`
`html`WARNING
If an attribute specifies a function as value, the function is called with the
entire attribute object as argument (incl. any id or class attribs derived
from an Emmet-style tag name). This allows for the dynamic generation of
attribute values, based on existing ones. The result MUST be a string.
`ts`
["div#foo", { bar: (attribs) => attribs.id + "-bar" }]
`html`
Function values for event attributes (any attrib name starting with
"on") WILL BE OMITTED from output:
`ts`
["div#foo", { onclick: () => alert("foo") }, "click me!"]
`html`click me!
`ts`
["div#foo", { onclick: "alert('foo')" }, "click me!"]
`html`click me!
`ts
import { serialize } from "@thi.ng/hiccup";
const thumb = (src) => ["img.thumb", { src, alt: "thumbnail" }];
serialize(
["div.gallery", ["foo.jpg", "bar.jpg", "baz.jpg"].map(thumb)]
);
`
`html`



Every component function will receive an arbitrary user defined context object
as first argument. This context object can be passed to
serialize()
via its [options argument]() and is then passed as arg to every component function
call.
The context object should contain any global component configuration,
e.g. for theming purposes.
`ts
import { serialize } from "@thi.ng/hiccup";
const header = (ctx, body) =>
["h1", ctx.theme.title, body];
const section = (ctx, title, ...body) =>
["section", ctx.theme.section, [header, title], ...body];
// theme definition (here using Tachyons CSS classes,
// but could be any attributes)
const theme = {
section: { class: "bg-black moon-gray bt b--dark-gray mt3" },
title: { class: "white f3" }
};
serialize(
[section, "Hello world", "Easy theming"],
// pass context object via options
{ ctx: { theme } }
);
// Hello world
Easy theming
`
Note: Of course the context is ONLY auto-injected for lazily embedded
component functions (like the examples shown above), i.e. if the functions are
wrapped in arrays and only called during serialization. If you call such a
component function directly, you MUST pass the context (or null) as first arg
yourself. Likewise, if a component function doesn't make use of the context you
can use either:
`ts
import { serialize } from "@thi.ng/hiccup";
// skip the context arg and require direct invocation
const div = (attribs, body) => ["div", attribs, body];
serialize(div({id: "foo"}, "bar"));
//
Or...
`ts
import { serialize } from "@thi.ng/hiccup";// ignore the first arg (context) and support both direct & indirect calls
const div = (_, attribs, body) => ["div", attribs, body];
// direct invocation of div (pass
null as context)
serialize(div(null, {id: "foo"}, "bar"));
// bar// lazy invocation of div
serialize([div, {id: "foo"}, "bar"]);
//
bar
`$3
Also see
@thi.ng/hiccup-svg
and
@thi.ng/geom
for related (and more advanced) functionality.
`ts tangle:export/readme-circles.js
import { serialize } from "@thi.ng/hiccup";
import { repeatedly } from "@thi.ng/transducers";
import { writeFileSync } "node:fs";// creates an unstyled SVG circle element
// we ignore the first arg (an auto-injected context arg)
// context handling is described further below
const circle = (_, x, y, r) => ["circle", { cx: ~~x, cy: ~~y, r: ~~r }];
// note how this next component lazily composes
circle.
// This form delays evaluation of the circle component
// until serialization time.
// since circle is in the head position of the returned array
// all other elements are passed as args when circle is called
const randomCircle = () => [
circle,
Math.random() * 1000,
Math.random() * 1000,
Math.random() * 100
];// generate 100 random circles and write serialized SVG to file
//
randomCircle is wrapped
import { XML_SVG } from "@thi.ng/prefixes";const doc = [
"svg", { xmlns: XML_SVG, width: 1000, height: 1000 },
["g", { fill: "none", stroke: "red" },
repeatedly(randomCircle, 100)]];
writeFileSync("export/circles.svg", serialize(doc));
`Resulting example output:
`xml
`$3
`js tangle:export/readme-glossary.js
import { serialize } from "@thi.ng/hiccup";// data
const glossary = {
foo: "widely used placeholder name in computing",
bar: "usually appears in combination with 'foo'",
hiccup: "de-facto standard format to define HTML in Clojure",
toxi: "author of this fine library",
};
// mapping function to produce single definition list item (pair of
/ tags)
const dlItem = (index, key) => [["dt", key], ["dd", index[key]]];// Helper function: takes a function
f and object items,
// executes fn for each key (sorted) in object and returns array of results
const objectList = (f, items) => Object.keys(items).sort().map((k)=> f(items, k));// full definition list component
const dlList = (_, attribs, items) => ["dl", attribs, objectList(dlItem, items)];
// finally the complete widget
const widget = [
"div.widget",
["h1", "Glossary"],
[dlList, { id: "glossary" }, glossary]];
// serialize with enforced HTML entity encoding (off by default)
console.log(serialize(widget, { escape: true }));
`(Re)formatted output (generated HTML will always be dense, without intermittent
white space):
`html
`$3
`js tangle:export/readme-toc.js
import { serialize } from "@thi.ng/hiccup";// stateful component to create hierarchically
// indexed & referencable section headlines:
// e.g. "sec-1.1.2.3"
const indexer = (prefix = "sec") => {
let counts = new Array(6).fill(0);
return (_, level, title) => {
counts[level - 1]++;
counts.fill(0, level);
return [
["a", { name: "sec-" + counts.slice(0, level).join(".") }],
["h" + level, title]
];
};
};
const TOC = [
[1, "Document title"],
[2, "Preface"],
[3, "Thanks"],
[3, "No thanks"],
[2, "Chapter"],
[3, "Exercises"],
[4, "Solutions"],
[2, "The End"]
];
// create new indexer instance
const section = indexer();
console.log(
serialize([
"div.toc",
TOC.map(([level, title]) => [section, level, title])
])
);
`Re-formatted HTML output:
`html
`$3
The sibling library
@thi.ng/hdom
supports components with basic life cycle methods (init, render, release). To
support serialization of hdom component trees, hiccup too supports such
components since version 2.0.0. For static serialization only the
render
method is of interest and others are ignored.`js
const component = {
render: (ctx, title, ...body) => ["section", ["h1", title], ...body]
};serialize([component, "Hello world", "Body"]);
`$3
The following attributes can be used to control the serialization
behavior of individual elements / tree branches:
-
__escape - boolean flag to enable/disable entity escaping
- __skip - if true, skips serialization (also used by
@thi.ng/hdom)
- __serialize - if false, skips serialization (hiccup only)`js
serialize(["div.container", ["div", { __skip: true }, "ignore me"]]);
//
`$3
Single or multiline comments can be included using the special
COMMENT
tag (or __COMMENT__, always WITHOUT attributes!).`ts
import { COMMENT } from "@thi.ng/hiccup";[COMMENT, "Hello world"]
// serializes to:
//
[COMMENT, "Hello", "world"]
//
`$3
Pre-serialized markup can be inlined without any further processing using the
special
INLINE tag (or __INLINE__, always WITHOUT attributes!):`js
import { INLINE, serialize } from "@thi.ng/hiccup";serialize(
["div", {}
[INLINE, "
Inlined & Embedded
Lorem ipsum...
"]
]
);
// Inlined & Embedded
Lorem ipsum...
`$3
Currently, the only processing / DTD instructions supported are:
-
?xml
- !DOCTYTPE
- !ELEMENT
- !ENTITY
- !ATTLISTThese are used as follows (attribs are only allowed for
?xml, all
others only accept a body string which is taken as is):`ts
["?xml", { version: "1.0", standalone: "yes" }]
// ["!DOCTYPE", "html"]
//
`Emitted processing instructions are always succeeded by a newline
character.
API
The library exposes these two functions:
$3
Signature:
serialize(tree: any, ctx?: any, escape = false): stringRecursively normalizes and serializes given tree as HTML/SVG/XML string.
Expands any embedded component functions with their results. Each node
of the input tree can have one of the following input forms:
`ts
["tag", ...]
["tag#id.class1.class2", ...]
["tag", {other: "attrib"}, ...]
["tag", {...}, "body", function, ...]
[function, arg1, arg2, ...]
[{render: (ctx,...) => [...]}, args...]
iterable
`Tags can be defined in "Zencoding" convention, e.g.
`ts
["div#foo.bar.baz", "hi"]
//
`The presence of the attributes object (2nd array index) is optional. Any
attribute values, incl. functions are allowed. If the latter, the
function is called with the full attribs object as argument and the
return value is used for the attribute. This allows for the dynamic
creation of attrib values based on other attribs. The only exception to
this are event attributes, i.e. attribute names starting with "on".
`ts
["div#foo", { bar: (attribs) => attribs.id + "-bar" }]
//
`The
style attribute can ONLY be defined as string or object.`ts
["div", { style: { color: "red", background: "#000" } }]
//
`Boolean attribs are serialized in HTML5 syntax (present or not). null or
empty string attrib values are ignored.
Any
null or undefined array values (other than in head position) will be
removed, unless a function is in head position.A function in head position of a node acts as a mechanism for component
composition & delayed execution. The function will only be executed at
serialization time. In this case the optional global context object and
all other elements of that node / array are passed as arguments when
that function is called. The return value the function MUST be a valid
new tree (or undefined).
`ts
import { serialize } from "@thi.ng/hiccup";const foo = (ctx, a, b) => ["div#" + a, ctx.foo, b];
serialize([foo, "id", "body"], { foo: { class: "black" } })
//
body
`Functions located in other positions are called ONLY with the global
context arg and can return any (serializable) value (i.e. new trees,
strings, numbers, iterables or any type with a suitable .toString()
implementation).
Please also see list of supported behavior control
attributes.
Authors
If this project contributes to an academic publication, please cite it as:
`bibtex
@misc{thing-hiccup,
title = "@thi.ng/hiccup",
author = "Karsten Schmidt",
note = "https://thi.ng/hiccup",
year = 2016
}
``© 2016 - 2026 Karsten Schmidt // Apache License 2.0