npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@varavel/nodx

v1.0.0

Published

NodX implementation for TypeScript

Readme


NodX lets you write HTML as plain TypeScript functions. No template strings, no JSX, no new syntax to learn.

You get type safety, automatic escaping, and readable code - and the HTML it generates is exactly what you'd expect.

Key Features

  • Zero dependencies - tiny, fast, and easy to keep updated.
  • Type safe - your editor catches typos in tags and attributes before you run anything.
  • Secure by default - text and attribute values are escaped automatically. No XSS surprises.
  • Readable output - what you write is what gets rendered, without hidden magic.
  • Familiar - if you know HTML and TypeScript, you already know NodX.

Installation

# npm (Node, Deno, Bun, etc.)
npm install --save --save-exact @varavel/nodx

# JSR (Deno)
deno add --save-exact jsr:@varavel/nodx

Quick Start

Pick the import style you like. Both do the same thing.

A) Named imports - explicit and tree-shakeable:

import {
  Body,
  Class,
  ClassMap,
  Div,
  DocType,
  Group,
  H1,
  Head,
  Html,
  If,
  P,
  TitleEl,
} from "@varavel/nodx";

const happiness = 100;

const page = Group(
  DocType(),
  Html(
    Head(
      TitleEl("My NodX Page"),
    ),
    Body(
      Div(
        ClassMap({
          container: true,
          hidden: false,
        }),
        H1(
          Class("title"),
          "Welcome to NodX!",
        ),
        P("This is a type-safe HTML generator for TypeScript."),
        If(
          happiness > 90,
          P(`With NodX, you will be ${happiness}% happy!`),
        ),
      ),
    ),
  ),
);

console.log(page.render());

B) Namespace import - no need to list everything:

import * as N from "@varavel/nodx";

const happiness = 100;

const page = N.Group(
  N.DocType(),
  N.Html(
    N.Head(
      N.TitleEl("My NodX Page"),
    ),
    N.Body(
      N.Div(
        N.ClassMap({
          container: true,
          hidden: false,
        }),
        N.H1(
          N.Class("title"),
          "Welcome to NodX!",
        ),
        N.P("This is a type-safe HTML generator for TypeScript."),
        N.If(
          happiness > 90,
          N.P(`With NodX, you will be ${happiness}% happy!`),
        ),
      ),
    ),
  ),
);

console.log(page.render());

Both render the same HTML:

<!DOCTYPE html>
<html>
  <head>
    <title>My NodX Page</title>
  </head>
  <body>
    <div class="container">
      <h1 class="title">Welcome to NodX!</h1>
      <p>This is a type-safe HTML generator for TypeScript.</p>
      <p>With NodX, you will be 100% happy!</p>
    </div>
  </body>
</html>

All HTML in this README is formatted for readability. NodX itself emits everything minified by default (no line breaks or extra whitespace) - same content, just compact.

From here on we'll use named imports for brevity, but anything like Div(...) can be written as N.Div(...) if you prefer the namespace style.


How it works

Elements are just functions

Call a tag like a function. Pass children, attributes, groups, or plain text - strings are escaped automatically, just like Text().

import { Class, Div, H1, P } from "@varavel/nodx";

const node = Div(
  Class("container"),
  H1(Text("Hello, NodX!")), // Text() is optional, you can use a string directly
  P("Build clean and safe HTML effortlessly."),
);

console.log(node.render());
<div class="container">
  <h1>Hello, NodX!</h1>
  <p>Build clean and safe HTML effortlessly.</p>
</div>

Attributes are functions too

Same idea for attributes. Src, Alt, Href - you get autocomplete and type checking for all of them.

import { Alt, Img, Src } from "@varavel/nodx";

console.log(
  Img(
    Src("image.jpg"),
    Alt("A beautiful image"),
  ).render(),
);
<img
  src="image.jpg"
  alt="A beautiful image"
>

There are boolean attributes e.g.: Disabled() means Disabled(true), Disabled(false) removes it.

import { Checked, Disabled, Input } from "@varavel/nodx";

console.log(
  Input(
    Checked(),
    Disabled(false),
  ).render(),
);

console.log(
  Input(
    Checked(false),
  ).render(),
);

console.log(
  Input(
    Disabled(),
  ).render(),
);
<input checked>
<input>
<input disabled>

Classes and styles that react to data

ClassMap and StyleMap keep conditional classes readable. Only truthy entries make it to the output.

import { ClassMap, Div, StyleMap } from "@varavel/nodx";

const node = Div(
  ClassMap({
    visible: true,
    hidden: false,
  }),
  StyleMap({
    "border: 1px solid black": true,
    "padding: 10px": false,
  }),
  "Conditional styling made simple!",
);

console.log(node.render());
<div
  class="visible"
  style="border: 1px solid black"
>
  Conditional styling made simple!
</div>

Conditionals and loops without a template language

No {{if}} or {{each}}. Just If, Map, and Eval.

import { Div, Eval, Group, If, Li, Map, Ul } from "@varavel/nodx";

const items = ["one", "two", "three"];
const isAdmin = true;
const score = 42;

const node = Group(
  If(
    isAdmin,
    Div("Admin only!"),
  ),
  // lazy version - the function only runs if isAdmin is true
  If(
    isAdmin,
    () => Div("Lazily rendered"),
  ),
  Ul(
    Map(
      items,
      (item) => Li(item),
    ),
  ),
  Eval(() => score > 40 ? Div("High score!") : Div("Keep trying")),
);

console.log(node.render());
<div>
  Admin only!
</div>
<div>
  Lazily rendered
</div>
<ul>
  <li>one</li>
  <li>two</li>
  <li>three</li>
</ul>
<div>
  High score!
</div>

Map flattens arrays for you, so you don't need to spread. null, undefined, and false are ignored - great for inline conditionals.

Components are just functions

If it returns a Node, it's a component. No special API.

import { Button, Class, Div, type Node } from "@varavel/nodx";

function PrimaryButton(label: string): Node {
  return Button(
    Class(
      "btn btn-sm",
      "btn-primary",
    ),
    label,
  );
}

console.log(
  Div(
    PrimaryButton("Save"),
    PrimaryButton("Cancel"),
  ).render(),
);
<div>
  <button class="btn btn-sm btn-primary">Save</button>
  <button class="btn btn-sm btn-primary">Cancel</button>
</div>

With the namespace import it looks the same:

import * as N from "@varavel/nodx";
import type { Node } from "@varavel/nodx";

function PrimaryButton(label: string): Node {
  return N.Button(
    N.Class(
      "btn btn-sm",
      "btn-primary",
    ),
    label,
  );
}

When you need something custom

Everything standard is generated for you, but you can always drop down to the primitives:

import { Attr, El, ElVoid } from "@varavel/nodx";

console.log(
  El(
    "my-widget",
    "Hello",
  ).render(),
);

console.log(
  ElVoid("my-decoration").render(),
);

console.log(
  El(
    "div",
    Attr(
      "data-user",
      "7",
    ),
    "hi",
  ).render(),
);
<my-widget>
  Hello
</my-widget>
<my-decoration>
<div data-user="7">
  hi
</div>

El is a normal element (with closing tag), ElVoid is for void elements (no closing tag), and Attr lets you create any attribute.

Naming collisions are already handled

Following the NodX spec, names that would clash get a predictable suffix:

| Why it collides | What you use | | ----------------------------------- | ---------------------------------------------------------------------------------- | | Element vs attribute with same name | DataEl (element <data>) / Data (attribute data-*), TitleEl / TitleAttr | | Reserved JS global | MapEl (element <map>), ObjectEl (element <object>) |

Everything else is just PascalCase of the HTML name: Div, Input, Class, Href. You rarely need to think about it - autocomplete shows you the right one.


NodX vs. other approaches

| | NodX | Template engine | JS framework | | --------------- | --------------------------------- | ---------------------------- | ------------------------ | | You write | functions (Div(), Class()) | files with {{ }} | JSX | | Type safety | full - compiler checks every tag | none - typos fail at runtime | partial | | New syntax? | none | its own mini-language | JSX + framework concepts | | Best for | server HTML, emails, static sites | classic server pages | highly interactive UIs |

If your page is mostly server-rendered and you want safety without learning a new templating language, NodX fits well.

Documentation

Full API reference — every element, attribute, helper and type, with examples — is on JSR:

https://jsr.io/@varavel/nodx/doc

If you want to browse what's available or check a specific tag's signature, start there.

Tailwind CSS

Get autocomplete for classes inside Class(...) and ClassMap({...}) by adding this to your VS Code settings:

{
  "tailwindCSS.includeLanguages": {
    "javascript": "javascript",
    "typescript": "typescript"
  },
  "tailwindCSS.experimental.classRegex": [
    [
      "Class\\(([^)]*)\\)",
      "[\"`]([^\"`]*)[\"`]"
    ],
    [
      "ClassMap\\(([^)]*)\\)",
      "[\"`]([^\"`]*)[\"`]"
    ]
  ]
}

It works with both import styles (Class(...) and N.Class(...)).

License

MIT - see LICENSE.