Need help with htl?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

161 Stars 7 Forks ISC License 56 Commits 2 Opened issues


Hypertext Literal

Services available


Need anything else?

Contributors list

# 18
53 commits
# 32,484
2 commits
# 4,149
1 commit

Hypertext Literal

Hypertext Literal is a tagged template literal for HTML which interpolates values based on context, allowing automatic escaping and the interpolation of non-serializable values, such as event listeners, style objects, and other DOM nodes. It is inspired by lit-html and HTM, and references the fantastically precise HTML5 spec.

Hypertext Literal is open-sourced under the permissive ISC license, small (2KB), has no dependencies, and is available on npm. To install:

npm install htl

In the near future, we plan to incorporate Hypertext Literal into the Observable standard library as the preferred method of generating HTML and SVG on Observable. Please help us improve by filing issues and sharing your feedback.

See this README with live examples on Observable:

Why not concatenate?

Surely the simplest way to generate web content is to write HTML. Modern JavaScript makes it easier than ever to interpolate values into literal HTML thanks to template literals.

const value1 = "world";
const html = `

Hello ${value1}


Yet simple concatenation has two significant drawbacks.

First, it confounds markup with text and other content. If an interpolated value happens to include characters that are meaningful markup, the result may render unexpectedly. An ampersand (&) can be interpreted as a character entity reference, for instance.

const value2 = "dollars&pounds";
const html = `My favorite currencies are ${value2}.`;

This can be fixed by escaping (say replacing ampersands with the corresponding entity,

). But you must remember to escape every time you interpolate, which is tedious! And it’s easy to forget when many values work as intended without it.
const html = `My favorite currencies are ${value2.replace(/&/g, "&")}.`;

Second, concatenation impedes composition: interpolated content must be serialized as markup. You cannot combine literal HTML with content created by the DOM API, or a library such as React or D3. And some content, such as event listeners implemented as closures, can’t be serialized!


Hypertext Literal is a tagged template literal that renders the specified markup as an element, text node, or null as appropriate.

html`I’m an element!` // returns an  element
html`I’m simply text.` // returns a text node
html`` // returns null

If multiple top-level nodes are given, the nodes are implicitly wrapped in a SPAN element.

html`I’m an implicit span.` // returns a  element

If you’d prefer a document fragment instead, as when composing hypertext literal fragments, call html.fragment.

html.fragment`I’m a document fragment.` // returns a DocumentFragment

Automatic escaping

If a value is interpolated into an attribute value or data, it is escaped appropriately so as to not change the structure of the surrounding markup.

html`Look, Ma, ${"automatic escaping"}!`
html`This text has color.`

In cases where it is not possible to interpolate safely, namely with script and style elements where the interpolated value contains the corresponding end tag, an error is thrown.

html`"}` // Error: unsafe raw text


You can safely interpolate into style properties, too, by specifying the style attribute as an object literal.

html`It’s all yellow!`

You can interpolate into a style attribute as a string, too, but use caution: automatic escaping will still allow you to set multiple style properties this way, or to generate invalid CSS.

html`It’s yellow (and italic).`

Function attributes

If an attribute value is a function, it is assigned as a property. This can be used to register event listeners.

html` alert("hello!")}>click me`

Boolean attributes

If an attribute value is false, it’s as if the attribute hadn’t been specified. If an attribute value is true, it’s equivalent to the empty string.

html`Can’t click me`

Optional values

If an attribute value is null or undefined, it’s as if the attribute hadn’t been specified. If a data value is null or undefined, nothing is embedded.

html`Can click me` // enabled!
html`There’s no ${null} here.` // “There’s no  here.”
html`${html``}` // returns null

Spread attributes

You can set multiple attributes (or styles, or event listeners) by interpolating an object in place of attributes.

html`hover me`

Node values

If an interpolated data value is a node, it is inserted into the result at the corresponding location. So if you have a function that generates a node (say itself using hypertext literal), you can embed the result into another hypertext literal.

function emphasize(text) {
  return html`${text}`;
html`This is ${emphasize("really")} important.`

Iterable values

You can interpolate iterables into data, too, even iterables of nodes. This is useful for mapping data to content via or Array.from. Typically, you should use html.fragment for the embedded expressions.

${, i) => html.fragment``)}
# Color Swatch
${i} ${color}
html`It’s as easy as ${new Set([1, 2, 3])}.`


You can create contextual SVG fragments using hypertext literals, too.


Errors on invalid bindings

Hypertext literal tolerates malformed input—per the HTML5 specification—but it still tries to be helpful by throwing an error if you interpolate a value into an unexpected place. For instance, it doesn’t allow dynamic tag names.

html`Does this work?>` // Error: invalid binding

How it works

Under the hood, hypertext literal implements a subset of the HTML5 tokenizer state machine. This allows it to distinguish between tags, attributes, and the like. And so wherever an embedded expression occurs, it can be interpreted correctly.

Our approach is more formal (and, if you like, more precise) than lit-html, which uses regular expressions to search for “attribute-like sequences” in markup. And while our approach requires scanning the input, the state machine is pretty fast.

Also unlike lit-html, hypertext literal directly creates content rather than reusable templates. Hypertext literal is thus well-suited to Observable, where our dataflow runtime runs cells automatically when inputs change. If you want incremental updates for performance (or transitions), you can opt-in, but it’s nice to keep things simple by default.

We also wanted to minimize new syntax. We were inspired by HTM, but HTM emulates JSX—not HTML5—requiring closing tags for every element. HTM’s approach would also need to be adapted for contextual namespaces, such as SVG, since it creates content bottom-up.

For a closer look at our implementation, please view the source and let us know what you think! We welcome your contributions and bug reports on GitHub.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.