HT.JS

html-in-javascript, made simple, done right.

Static Site Generator

import { writeFile } from "node:fs/promises";
import { html, head, body, meta, title, link, h1 } from "html-in-javascript";

const page = 
html({ lang: "en" },
    head(
        meta({ charset: "UTF-8" }),
        meta({ name: "viewport", content: "width=device-width, initial-scale=1.0" }),
        title("Welcome!"),
        link({ rel: "stylesheet", href: "/css/style.css" }),
    ),
    body({ class: "home-page" },
        h1("Hello World!"),
    )
)

await writeFile("public/index.html", page);

Server Side Rendered

import express from 'express';
import { html, head, body, meta, title, link, h1 } from "html-in-javascript";

const page = 
html({ lang: "en" },
    head(
        meta({ charset: "UTF-8" }),
        meta({ name: "viewport", content: "width=device-width, initial-scale=1.0" }),
        title("Welcome!"),
        link({ rel: "stylesheet", href: "/css/style.css" }),
    ),
    body({ class: "home-page" },
        h1("Hello World!"),
    )
)

const app = express();

app.get('/', (req, res) => {
    res.send(page);
})

app.listen(3000, () => console.log("http://localhost:3000"));

Bundled for Browser

// bundle.js
import * as esbuild from 'esbuild'

await esbuild.build({
    entryPoints: ['src/js/foo.js', 'src/js/bar.js', 'src/js/buff.js'],
    bundle: true,
    minify: true,
    sourcemap: true,
    splitting: true,
    format: "esm",
    target: "esnext",
    outdir: 'dist/js'
})
    

// foo.js
import { fragment, head, body, meta, title, link, h1 } from "html-in-javascript";

const page = 
fragment(
    head(
        meta({ charset: "UTF-8" }),
        meta({ name: "viewport", content: "width=device-width, initial-scale=1.0" }),
        title("Welcome!"),
        link({ rel: "stylesheet", href: "/css/style.css" }),
    ),
    body({ class: "home-page" },
        h1("Hello World!"),
    )
)

document.documentElement.innerHTML = page;

<!-- index.html -->
<html lang="en">
    <script src="/js/bundle.js" type="module"></script>
</html>

Directly in Browser

// foo.js
import { fragment, head, body, meta, title, link, h1 } from "https://cdn.jsdelivr.net/npm/html-in-javascript/esm.js";

const page = 
fragment(
    head(
        meta({ charset: "UTF-8" }),
        meta({ name: "viewport", content: "width=device-width, initial-scale=1.0" }),
        title("Welcome!"),
        link({ rel: "stylesheet", href: "/css/style.css" }),
    ),
    body({ class: "home-page" },
        h1("Hello World!"),
    )
)

document.documentElement.innerHTML = page;

<!-- index.html -->
<html lang="en">
    <script src="/js/foo.js" type="module"></script>
</html>

Docs

Code
div({
        // Objects are the element's attributes
        // in a key:value relationship eg.
        class: "class names",
        // becomes: class="class names" 
        id: "some-id",
        "data-test-id": 123,
        onclick: "this.style.color = 'blue'",
        style: "color:red; padding:10px; border:1px solid black"
    },
    h2("hello world!"),
    p(
        "All of these 'element functions' eg. h2(), p(), strong() etc. return strings of HTML",
        br(),
        em("and can be nested exactly like html")
    ),
    {
        // you can have more than one object for the attributes
        "data-foo": "bar"
        // and they will be combined, but probably better to
        // just have one attribute object, and to put it first
    }
)
HTML
<div 
  class="class names"
  id="some-id" 
  data-test-id="123" 
  onclick="this.style.color = 'blue'" 
  style="color:red; padding:10px; border:1px solid black"
  data-foo="bar">
    <h2>hello world!</h2>
    <p>All of these 'element functions' eg. h2(), p(), strong() etc. return strings of HTML<br><em>and can be nested exactly like html</em></p>
</div>
Example

hello world!

All of these 'element functions' eg. h2(), p(), strong() etc. return strings of HTML
and can be nested exactly like html

fragment

similar to React's <>{content}</>

Used when you can't, or don't want a container <div>

Code
const titleAndStyle = fragment(
    title("HT.JS"),
    link({ rel:"stylesheet", href:"/css/style.css" })
)

head(
    meta({ charset:"UTF-8" }),
    meta({ name:"viewport", content:"width=device-width, initial-scale=1.0" }),
    titleAndStyle
)
HTML
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HT.JS</title>
    <link rel="stylesheet" href="/css/style.css">
</head>

Inline scripts

event, import, then default

It's 2024! javascript modules are very well supported, and you can import them directly in the browser.

So instead of including a script in the head and exposing a function globally, you can import the script directly in the element's onclick event.

Code
// increaseTextSize.js
export default (el) => {
    const computedFontSize = parseFloat(window.getComputedStyle(el).fontSize);
    el.style.fontSize = (computedFontSize * 1.1) + "px"
}

// index.js
button({
    onclick: "import('/js/increaseTextSize.js').then(M => M.default(this))"
}, "Increase Text Size")
Example

Extensible

At it's core HT.JS is just a collection of functions, so you can easily add your own to create reusable components.

// pageHead.js
import { head, meta, link, title, fragment } from "html-in-javascript";

const deviceMeta = fragment(
    meta({
        charset: "UTF-8"
    }),
    meta({
        name: "viewport", 
        content: "width=device-width, initial-scale=1.0"
    })
)

const favicons = fragment(
    link({
        rel: "apple-touch-icon", 
        sizes: "180x180", 
        href: "/apple-touch-icon.png" 
    }),
    link({
        rel: "icon", 
        type: "image/png", 
        sizes: "32x32", 
        href: "/favicon-32x32.png" 
    }),
    link({
        rel: "icon", 
        type:"image/png", 
        sizes: "16x16", 
        href:"/favicon-16x16.png"
    }),
    link({
        rel: "manifest", 
        href: "/site.webmanifest" 
    }),
    link({
        rel: "mask-icon", 
        href: "/safari-pinned-tab.svg", 
        color: "#5bbad5"
    }),
    meta({
        name: "msapplication-TileColor", 
        content: "#2d89ef" 
    }),
    meta({
        name: "theme-color", 
        content: "#ffffff" 
    })
)

export default ({
        pageTitle = "example.com", 
        description = "Placeholder description",
    } = {}, 
    ...rest
) => 
head(
    deviceMeta,
    favicons,
    title(pageTitle),
    link({
        rel: "stylesheet", 
        href: "/css/style.css"
    }),
    meta({
        name: "description", 
        content: description
    }),
    ...rest
)
// index.js
import { html, body, h1, p, script } from "html-in-javascript";
import pageHead from "./pageHead.js";

const page =
html({ lang: "en" },
    pageHead({
            pageTitle: "example.com | My Page", 
            description: "My page description"
        },
        script({ src:"/js/someScript.js" })
    ),
    body(
        h1("My Page"),
        p("This is my page")
    )
)
HTML
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
    <link rel="manifest" href="/site.webmanifest">
    <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
    <meta name="msapplication-TileColor" content="#2d89ef">
    <meta name="theme-color" content="#ffffff">
    <title>example.com | My Page</title>
    <link rel="stylesheet" href="/css/style.css">
    <meta name="description" content="My page description">
    <script src="/js/someScript.js"></script>
</head>
<body>
    <h1>My Page</h1>
    <p>This is my page</p>
</body>
</html>