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

mono-jsx-dom

v0.1.11

Published

A JSX runtime for browsers.

Readme

🔥 mono-jsx-dom

mono-jsx-dom

[!WARNING] This library is currently under active development. The API may change at any time. Use at your own risk. Please report any issues or feature requests on the issues page.

mono-jsx-dom is a JSX runtime for building web user interface.

  • ⚡️ Use browser-specific APIs, no virtual DOM
  • 🦋 Lightweight (4KB gzipped), zero dependencies
  • 🚦 Signals as reactive primitives
  • 💡 Complete Web API TypeScript definitions
  • ⏳ Streaming rendering
  • 🎨 Builtin TailwindCSS integration
  • 🔩 Builtin dev/build/deploy toolchain

Playground: https://val.town/x/ije/mono-jsx-dom

Getting Started

You can run the mono-jsx-dom int command to initialize a project with mono-jsx-dom boilerplate.

# node
npx mono-jsx-dom init

# bun
bunx --bun mono-jsx-dom init

Usage

mono-jsx-dom adds a mount method to the HTMLElement prototype to allow you to mount the UI to the DOM.

// app.tsx

async function App(this: FC<{ word: string }>) {
  this.word = await fetch("/data/word").then(res => res.text());
  return <div>Hello, {this.word}!</div>;
}

document.body.mount(<App />);

You can also define a component as a custom element with the register function:

// app.tsx

import { register } from "mono-jsx-dom";

function App(this: FC<{ word: string }>) {
  return <div>Hello, {this.word}!</div>;
}

register("my-app", App, { mode: "open", style: "div { color: black; }" });

Then you can use the <my-app> element in your HTML:

<my-app word="world"></my-app>
<script type="module" src="app.tsx"></script>

[!TIP] mono-jsx-dom is designed for client-side rendering. You can use mono-jsx to render the UI on the server side.

Using JSX

mono-jsx-dom uses JSX to describe the user interface, similar to React but with key differences.

Using Standard HTML Property Names

mono-jsx-dom adopts standard HTML property names, avoiding React's custom naming conventions:

  • classNameclass
  • htmlForfor
  • onChangeonInput

Composition with class

mono-jsx-dom allows you to compose the class property using arrays of strings, objects, or expressions:

<div
  class={[
    "container box",
    isActive && "active",
    { hover: isHover },
  ]}
/>;

Using Pseudo-Classes and Media Queries in style

mono-jsx-dom supports pseudo-classes, pseudo-elements, media queries, and CSS nesting in the style property:

<a
  style={{
    display: "inline-flex",
    gap: "0.5em",
    color: "black",
    "::after": { content: "↩️" },
    ":hover": { textDecoration: "underline" },
    "@media (prefers-color-scheme: dark)": { color: "white" },
    "& .icon": { width: "1em", height: "1em" },
  }}
>
  <img class="icon" src="link.png" />
  Link
</a>;

Using <slot> Element

mono-jsx-dom uses <slot> elements to render slotted content (equivalent to React's children property). You can also add the name prop to define named slots:

function Container() {
  return (
    <div class="container">
      {/* Default slot */}
      <slot />
      {/* Named slot */}
      <slot name="desc" />
    </div>
  )
}

function App() {
  return (
    <Container>
      {/* This goes to the named slot */}
      <p slot="desc">This is a description.</p>
      {/* This goes to the default slot */}
      <h1>Hello world!</h1>
    </Container>
  )
}

Using html Tag Function

mono-jsx-dom injects a global html tag function to allow you to render raw HTML, which is similar to React's dangerouslySetInnerHTML.

function App() {
  const title = "Hello world!";
  return <div>{html`<h1>${title}</h1>`}</div>;
}

Variables in the html template literal are escaped. To render raw HTML without escaping, call the html function with a string literal.

function App() {
  const title = "<span style='color: blue;'>Hello world!</span>";
  return <div>{html(`<h1>${title}</h1>`)}</div>;
}

You can also use css and js functions for CSS and JavaScript:

function App() {
  return (
    <head>
      <style>{css`h1 { font-size: 3rem; }`}</style>
      <script>{js`console.log("Hello world!")`}</script>
    </head>
  )
}

[!WARNING] The html tag function is unsafe and can cause XSS vulnerabilities.

Event Handlers

mono-jsx-dom lets you write event handlers directly in JSX, similar to React:

function Button() {
  return (
    <button onClick={(evt) => alert("BOOM!")}>
      Click Me
    </button>
  )
}

mono-jsx-dom allows you to use a function as the value of the action prop of the <form> element. The function will be called on form submission, and the FormData object will contain the form data.

function App() {
  return (
    <form action={(data: FormData) => console.log(data.get("name"))}>
      <input type="text" name="name" />
      <button type="submit">Submit</button>
    </form>
  )
}

Async Components

mono-jsx-dom supports async components that return a Promise or are declared as async functions. With streaming rendering, async components are rendered asynchronously, allowing you to fetch data or perform other async operations before rendering the component.

async function JsonViewer(props: { url: string }) {
  const data = await fetch(props.url).then((res) => res.json());
  return <ObjectViewer data={data} />;
}

function App() {
  return (
    <JsonViewer url="https://example.com/data.json" />
  )
}

document.body.mount(<App />);

You can use pending to display a loading state while waiting for async components to render:

async function Sleep({ ms }) {
  await new Promise((resolve) => setTimeout(resolve, ms));
  return <slot />;
}

function App() {
  return (
    <Sleep ms={1000} pending={<p>Loading...</p>}>
      <p>After 1 second</p>
    </Sleep>
  )
}

document.body.mount(<App />);

Error Handling

You can add the catch prop to a function component. This allows you to catch errors in components and display a fallback UI:

async function Hello() {
  throw new Error("Something went wrong!");
  return <p>Hello world!</p>;
}

function App() {
  return (
    <Hello catch={err => <p>{err.message}</p>} />
  )
}

document.body.mount(<App />);

The catch prop should be a function that gets the caught error as the first argument and returns a JSX element.

Using Signals

mono-jsx-dom uses signals to update the view when a signal changes. Signals are similar to React's state, but they are lighter-weight and more efficient. You can use signals to manage state in your components.

Using Component Signals

You can use the this keyword in your components to manage signals. Signals are bound to the component instance, can be updated directly, and automatically re-render the view when they change:

function Counter(this: FC<{ count: number }>, props: { initialCount?: number }) {
  // Initialize a signal
  this.count = props.initialCount ?? 0;

  // or you can use `this.init` to initialize the signals
  this.init({ count: props.initialCount ?? 0 });

  return (
    <div>
      {/* render signal */}
      <span>{this.count}</span>

      {/* Update signal to trigger re-render */}
      <button onClick={() => this.count--}>-</button>
      <button onClick={() => this.count++}>+</button>
    </div>
  )
}

You can use this.store to create a signal store. You can use getters to create derived (computed) signals.

function App(this: FC<{ count: number }>) {
  const counter = this.store({
    value: 0,
    // double is a derived(computed) signal
    get double() {
      return this.value * 2;
    }
  });

  return (
    <div>
      <span>count:{counter.value}</span>
      <span>double: {counter.double}</span>
      <button onClick={() => counter.value++}>+</button>
    </div>
  )
}

Using atom and store

mono-jsx-dom provides two functions that allow you to define shared global signals. You can use them to share signals between components.

  • atom(initValue): Creates an atom signal.
  • store(initValue): Creates a signal store.
export interface Atom<T> {
  get(): T;
  set(value: T | ((prev: T) => T)): void;
  map(callback: (value: T extends (infer V)[] ? V : T, index: number) => JSX.ChildPrimitiveType): JSX.ChildPrimitiveType[];
  ref(): T;
  ref<V>(callback: (value: T) => V): V;
}

export const atom: <T>(initValue: T) => Atom<T>;
export const store: <T extends Record<string, unknown>>(initValue: T) => T;

Example:

import { atom, store } from "mono-jsx-dom";

const count = atom(0);
const store = store({ text: 'Count:' });

function Counter(this: FC) {
  this.effect(() => {
    console.log("count changed:", count.get());
  });
  return (
   <span>{store.text}{count}</span>
  )
}

function Buttons(this: FC) {
  return (
    <>
      <button onClick={() => count.set(prev => prev+1)}>+</button>
      <button onClick={() => store.text = store.text === 'Count:' ? 'Count:' : '计数:'}>English/中文</button>
    </>
  )
}

function App(this: FC) {
  return (
    <>
      <Counter />
      <Buttons />
    </>
  )
}

document.body.mount(<App />);

Using Computed Signals

You can use this.computed to create a derived signal based on other signals:

function App(this: FC<{ input: string }>) {
  this.input = "Welcome to mono-jsx";
  return (
    <div>
      <h1>{this.computed(() => this.input + "!")}</h1>
      <input type="text" $value={this.input} />
    </div>
  )
}

[!TIP] You can use this.$ as a shorthand for this.computed to create computed signals.

Using Effect

You can use this.effect to perform side effects in components. The effect runs when the component is mounted, automatically collects used signals as dependencies, and reruns when those dependencies change.

function App(this: FC<{ count: number }>) {
  this.count = 0;

  this.effect(() => {
    console.log("Count:", this.count);
  });

  return (
    <div>
      <span>{this.count}</span>
      <button onClick={() => this.count++}>+</button>
    </div>
  )
}

The callback function of this.effect can return a cleanup function that runs once the component's element has been removed through <show>, <hidden>, or <switch> conditional rendering:

function Counter(this: FC<{ count: number }>) {
  this.count = 0;

  this.effect(() => {
    const interval = setInterval(() => {
      this.count++;
    }, 1000);

    return () => clearInterval(interval);
  });

  return (
    <div>
      <span>{this.count}</span>
    </div>
  )
}

function App(this: FC<{ show: boolean }>) {
  return (
    <div>
      <show when={this.show}>
        <Counter />
      </show>
      <button onClick={() => this.show = !this.show }>{this.$(() => this.show ? 'Hide': 'Show')}</button>
    </div>
  )
}

Using <show> Element with Signals

The <show> element conditionally renders content based on the when prop. You can use signals to control the visibility of the content on the client side.

function App(this: FC<{ show: boolean }>) {
   const toggle = () => {
    this.show = !this.show;
  }

  return (
    <div>
      <show when={this.show}>
        <h1>Welcome to mono-jsx!</h1>
      </show>

      <button onClick={toggle}>
        {this.$(() => this.show ? "Hide" : "Show")}
      </button>
    </div>
  )
}

mono-jsx-dom also provides a <hidden> element that is similar to <show>, but it conditionally hides the content based on the when prop.

function App(this: FC<{ hidden: boolean }>) {
  return (
    <div>
      <hidden when={this.hidden}>
        <h1>Welcome to mono-jsx!</h1>
      </hidden>
    </div>
  )
}

If you need if-else logic in JSX, use the <switch> element instead:

function App(this: FC<{ ok: boolean }>) {
  return (
    <div>
      <switch value={this.ok}>
        <span slot="true">True</span>
        <span slot="false">False</span>
      </switch>
    </div>
  )
}

Using <switch> Element with Signals

The <switch> element renders different content based on the value prop. Elements with matching slot props are displayed when their value matches, otherwise default slots are shown. Like <show>, you can use signals to control the value on the client side.

function App(this: FC<{ lang: "en" | "zh" | "🙂" }>) {
  this.lang = "en";

  return (
    <div>
      <switch value={this.lang}>
        <h1 slot="en">Hello, world!</h1>
        <h1 slot="zh">你好,世界!</h1>
        <h1 slot="🙂">✋🌎❗️</h1>
      </switch>
      <p>
        <button onClick={() => this.lang = "en"}>English</button>
        <button onClick={() => this.lang = "zh"}>中文</button>
        <button onClick={() => this.lang = "🙂"}>🙂</button>
      </p>
    </div>
  )
}

Form Input Two-way Binding

You can use the $value prop to bind a signal to the value of a form input element. The $value prop provides two-way data binding, which means that when the input value changes, the signal is updated, and when the signal changes, the input value is updated.

function App(this: FC<{ value: string }>) {
  this.value = "Welcome to mono-jsx";
  this.effect(() => {
    console.log("value changed:", this.value);
  });
  // return <input value={this.value} oninput={e => this.value = e.target.value} />;
  return <input $value={this.value} />;
}

You can also use the $checked prop to bind a signal to the checked state of a checkbox or radio input.

function App(this: FC<{ checked: boolean }>) {
  this.effect(() => {
    console.log("checked changed:", this.checked);
  });
  // return <input type="checkbox" checked={this.checked} onchange={e => this.checked = e.target.checked} />;
  return <input type="checkbox" $checked={this.checked} />;
}

Limitations of Signals

1. Arrow functions are non-stateful components.

// ❌ Won't work - uses `this` in a non-stateful component
const App = () => {
  this.count = 0;
  return (
    <div>
      <span>{this.count}</span>
      <button onClick={() => this.count++}>+</button>
    </div>
  )
};

// ✅ Works correctly
function App(this: FC) {
  this.count = 0;
  return (
    <div>
      <span>{this.count}</span>
      <button onClick={() => this.count++}>+</button>
    </div>
  )
}

2. Signals would not be computed automatically outside of the this.computed method.

// ❌ Won't work - updates of a signal won't refresh the view
function App(this: FC<{ message: string }>) {
  this.message = "Welcome to mono-jsx";
  return (
    <div>
      <h1>{this.message + "!"}</h1>
      <button onClick={() => this.message = "Clicked"}>
        Click Me
      </button>
    </div>
  )
}

// ✅ Works correctly
function App(this: FC) {
  this.message = "Welcome to mono-jsx";
  return (
    <div>
      <h1>{this.computed(() => this.message + "!")}</h1>
      <button onClick={() => this.message = "Clicked"}>
        Click Me
      </button>
    </div>
  )
}

3. this in nested functions in a component function would not be bound to the component. You can use an arrow function that automatically binds this to the component.

function App(this: FC<{ count: number }>) {
  function increment() {
    this.count++; // ❌ `this` is not bound to the component.
  }
  return (
    <div>
      <span>{this.count}</span>
      <button onClick={increment}>{this.count}</button>
    </div>
  )
}

function App(this: FC) {
  const increment = () => {
    this.count++; // ✅ `this` is bound to the component.
  }
  return (
    <div>
      <span>{this.count}</span>
      <button onClick={increment}>{this.count}</button>
    </div>
  )
}

Using this in Components

mono-jsx-dom binds a scoped signals object to this in your component functions. This allows you to access signals, context, and request information directly in your components.

The this object has the following built-in properties:

  • atom(initValue): Creates an atom signal.
  • store(initValue): Creates a signal store.
  • init(initValue): Initializes the signals.
  • refs: A map of refs defined in the component.
  • computed(fn): A method to create a computed signal.
  • $(fn): A shortcut for computed(fn).
  • effect(fn): A method to create side effects.
type FC<Signals = {}, Refs = {}> = {
  atom<T>(initValue: T): Atom<T>;
  store<T extends Record<string, unknown>>(initValue: T): T;
  init(initValue: Signals): void;
  refs: Refs;
  computed<T = unknown>(fn: () => T): T;
  $: FC["computed"]; // A shortcut for `FC.computed`.
  effect(fn: () => void | (() => void)): void;
} & Signals;

Using Signals

See the Using Signals section for more details on how to use signals in your components.

Using Refs

You can use this.refs to access refs in your components. Refs are defined using the ref prop in JSX, and they allow you to access DOM elements directly. The refs object is a map of ref names to DOM elements.

function App(this: WithRefs<FC, { input?: HTMLInputElement }>) {
  this.effect(() => {
    this.refs.input?.addEventListener("input", (evt) => {
      console.log("Input changed:", evt.target.value);
    });
  });

  return (
    <div>
      <input ref={this.refs.input} type="text" />
      <button onClick={() => this.refs.input?.focus()}>Focus</button>
    </div>
  )
}

License

MIT