Skip to content
Alexander Zaytsev edited this page Sep 5, 2025 · 33 revisions

jsx-dom-runtime

It's the Babel plugin to transform JSX syntax to the DOM elements with minimal runtime dependency ~500 B. Support HTML, SVG, and MathML tags.

source code:

document.body.append(
  <main class="box">
    <h1 class="title">Hello, World!</h1>
  </main>
);

after compilation:

import { jsx as _jsx } from "jsx-dom-runtime";

document.body.append(
  _jsx("main", {
    class: "box"
  }, _jsx("h1", {
    class: "title"
  }, "Hello, World!"))
);

The Babel preset handles the injection of runtime functions, so no manual imports are required.

Install

npm i jsx-dom-runtime
# or
yarn add jsx-dom-runtime

Configuration

To enable JSX transformation, add the jsx-dom-runtime/babel-preset to your Babel configuration file (e.g., .babelrc or babel.config.json). This preset configures Babel to correctly transform JSX syntax into DOM elements using this library's runtime.

.babelrc

{
  "presets": [
    "jsx-dom-runtime/babel-preset"
  ]
}

Syntax

This library supports the standard JSX syntax, allowing you to write HTML-like code in your JavaScript files. Below are some examples of how to use different features.

Attributes

Write the attributes closer to HTML than to JavaScript

Use attribute class instead of the className DOM property as in React.

- <div className="box" />
+ <div class="box" />
  • Use for instead of htmlFor:
- <label htmlFor="cheese">Do you like cheese?</label>
+ <label for="cheese">Do you like cheese?</label>

Style

The style attribute supports the JavaScript object and a string value. Also, you can use CSS custom properties

<div style="background-color: #ffe7e8; border: 2px solid #e66465;" />
<div style="--color: red;" />
// or
<div style={{ backgroundColor: '#ffe7e8', border: '2px solid #e66465' }} />
<div style={{ '--color': 'red' }} />

SVG

Use unmodified SVG attributes instead of camelCase style as in React

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
-  <circle strokeWidth="2" strokeLinejoin="round" cx="24" cy="24" r="20" fill="none" />
+  <circle stroke-width="2" stroke-linejoin="round" cx="24" cy="24" r="20" fill="none" />
</svg>

Don't use namespaced attributes. The namespaced attributes are deprecated and no longer recommended.

Instead of xlink:href you should use href

<svg viewBox="0 0 160 40" xmlns="http://www.w3.org/2000/svg">
-  <a xlink:href="https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href">
+  <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href">
    <text x="10" y="25">MDN Web Docs</text>
  </a>
</svg>

Event handling

There are a few ways to add event handling to a DOM Element.

  1. Using the event handler properties that start with on* as onclick or ondblclick.
<button
  type="button"
  onclick={(event) => { }}
  ondblclick={(event) => { }}
>
  Click Me!
</button>;

Attention! In this way, the event listener will be assigned directly to the Element object as a property.

// Equivalent on vanilla JavaScript
button.onclick = (event) => { };
button.ondblclick = (event) => { };
  1. Using the namespace syntax for the event listener that start with on:* as on:change or on:focus.
<input
  type="text"
  on:change={(event) => { }}
  on:focus={(event) => { }}
/>

After the compilation, it registers the event with addEventListener

// Equivalent on vanilla JavaScript
input.addEventListener('change', (event) => { });
input.addEventListener('focus', (event) => { });
  1. Using ref callback. The callback will be called with the target element when it is created.
<button
  type="button"
  ref={(node) => {
    // Use capture phase
    node.addEventListener('click', (event) => { }, true);
    // With event options
    node.addEventListener('dblclick', (event) => { }, { once: true });
  }}>
  Click Me!
</button>;

Attribute Directives

Use the attr:* directive to set HTML attributes directly on elements. This is particularly useful for setting custom attributes, data attributes, or when you need to ensure a value is set as an attribute rather than a property.

<div
  attr:data-id="123"
  attr:aria-label="Custom label"
  attr:custom-attribute="value"
/>

The attr:* directive uses setAttribute() to set attributes on the DOM element:

// Equivalent on vanilla JavaScript
div.setAttribute('data-id', '123');
div.setAttribute('aria-label', 'Custom label');
div.setAttribute('custom-attribute', 'value');

Common use cases:

  • Setting custom data attributes
  • Setting attributes that must be strings
  • Ensuring attributes are represented in the HTML markup
// Set data attributes
<div attr:data-testid="submit-button" attr:data-track="click" />

// Set ARIA attributes
<button attr:aria-expanded="false" attr:aria-controls="menu" />

// Set custom attributes
<img attr:loading="lazy" attr:custom-src={imageUrl} />

// Boolean attributes (empty values become "true")
<input attr:required />

Value handling:

  • String and numeric values are converted to strings
  • Boolean true or empty attributes become "true"
  • null and undefined values are ignored
  • Objects are converted using toString()

Property Directives

Use the prop:* directive to set DOM properties directly on elements. This is useful when you need to set properties that don't have corresponding HTML attributes or when you want to bypass attribute parsing.

<div
  prop:id="my-div"
  prop:textContent="Hello World"
  prop:_customProperty={customValue}
/>

The prop:* directive sets properties directly on the DOM element, similar to how you would in vanilla JavaScript:

// Equivalent on vanilla JavaScript
div.id = "my-div";
div.textContent = "Hello World";
div._customProperty = customValue;

Common use cases:

  • Setting textContent or innerHTML directly
  • Setting any available element property directly
  • Working with custom properties on elements
  • Setting properties that behave differently than their attribute counterparts
// Set text content directly
<span prop:textContent="This text is set via property" />

// Set HTML content
<div prop:innerHTML="<p>This is <strong>HTML</strong> content</p>" />

// Add to classList
<div prop:classList="new-class" />

// Custom properties
<div prop:customData={complexObject} />

Note: Property directives are processed after attribute directives (attr:*) but before event handlers and refs.

Function Components

Function components must start with a capital letter or they won’t work.

const App = ({ name }) => (
  <div>Hello {name}</div>
);

document.body.append(<App name="Bob" />);

Fragments

Use <>...</> syntax to group multiple elements together without creating an extra wrapper element. This is useful when you need to return multiple elements from a component or when you want to avoid unnecessary DOM nesting. Under the hood, it uses the DocumentFragment interface, which provides efficient DOM manipulation.

document.body.append(
  <>
    <p>Hello</p>
    <p>World</p>
  </>
);

APIs

Creating Refs

Adding a reference to a DOM Element. When a ref is passed to an element in create, a reference to the node becomes accessible at the current attribute of the ref

import { useRef } from 'jsx-dom-runtime';

const ref = useRef();

const addItem = () => {
  // add an item to the list
  ref.current.append(<li>New Item</li>);
};

document.body.append(
  <>
    <button type="button" on:click={addItem}>
      Add Item
    </button>
    <ul ref={ref} />
  </>
);

Callback Refs

Another way to get the reference to an element can be done by passing a function callback. The callback will be called with the actual reference DOM element

const setRef = (node) => {
  node.addEventListener('focusin', () => {
    node.style.backgroundColor = 'pink';
  });

  node.addEventListener('focusout', () => {
    node.style.backgroundColor = '';
  });
};

document.body.append(
  <input type="text" ref={setRef} />
);

Text

Use the Text node in a DOM tree.

import { useText } from 'jsx-dom-runtime';

const [text, setText] = useText('The initial text');

const clickHandler = () => {
  setText('Clicked!');
};

document.body.append(
  <>
    <p>{text}</p>
    <button type="button" on:click={clickHandler}>
      Click me
    </button>
  </>
);

Template

Get template from a string.

import { Template } from 'jsx-dom-runtime';

document.body.append(
  <Template>
    {`<svg width="24" height="24" aria-hidden="true">
        <path d="M12 12V6h-1v6H5v1h6v6h1v-6h6v-1z"/>
      </svg>`}
  </Template>
);

extensions

Add custom attributes in JSX.Element.

import { extensions } from 'jsx-dom-runtime';

extensions
  .set('x-class', (node, value) => {
    node.setAttribute('class', value.filter(Boolean).join(' '));
  })
  .set('x-dataset', (node, value) => {
    for (let key in value) {
      if (value[key] != null) {
        node.dataset[key] = value[key];
      }
    }
  })
  .set('x-autofocus', (node, value) => {
    setTimeout(() => node.focus(), value);
  });

document.body.append(
  <input
    x-class={['one', 'two']}
    x-dataset={{ testid: 'test', hook: 'text' }}
    x-autofocus={1000}
  />
);

Result

<input class="one two" data-testid="test" data-hook="text">

TypeScript types definition for custom attributes:

global.d.ts

declare global {
  namespace JSX {
    // add types for all JSX elements
    interface Attributes {
      'x-class'?: string[];
      'x-dataset'?: Record<string, string>;
    }

    // add types only for Input elements
    interface HTMLInputElementAttributes {
      'x-autofocus'?: number;
    }
  }
}

export {};

ESLint Support

This library provides ESLint rules to help you write better JSX code and catch common mistakes. The rules are designed to work with ESLint v9 and help enforce best practices when using jsx-dom-runtime.

Configuration

Add the jsx-dom-runtime ESLint plugin to your eslint.config.js file (ESLint v9 flat config):

Option 1: Basic Configuration

Use the jsx-dom-runtime plugin with default settings:

eslint.config.js

import jsxDomRuntime from 'jsx-dom-runtime/eslint-plugin';

export default [
  jsxDomRuntime,
];

Option 2: Complete Setup with TypeScript

Use the pre-configured setup that includes TypeScript, JavaScript, and jsx-dom-runtime rules:

eslint.config.js

import { defineConfig } from 'eslint/config';
import eslint from '@eslint/js';
import tslint from 'typescript-eslint';
import jsxDomRuntime from 'jsx-dom-runtime/eslint-plugin';

export default defineConfig(
  eslint.configs.recommended,
  tslint.configs.recommended,
  jsxDomRuntime,
  {
    rules: {
      // Override jsx-dom-runtime default rule configurations
      'jsx-dom-runtime/no-spread-attribute-in-dom-element': 'error',
      'jsx-dom-runtime/no-children-in-void-element': 'error',
      'jsx-dom-runtime/no-spread-children': 'error',
      'jsx-dom-runtime/no-legacy-event-handler': 'warn',
      'jsx-dom-runtime/prefer-attributes-over-properties': 'error',
      'jsx-dom-runtime/jsx-import': 'warn',
      // Add your project-specific ESLint rules here (TypeScript, Prettier, etc.)
    },
  },
);

Available Rules

Rule Name Description Auto-fixable
jsx-dom-runtime/jsx-import Enforces importing from "jsx-dom-runtime" instead of "jsx-dom-runtime/jsx-runtime" import { jsx } from "jsx-dom-runtime/jsx-runtime"import { jsx } from "jsx-dom-runtime"
jsx-dom-runtime/no-children-in-void-element Prevents adding children to void HTML elements (<img/>, <br/>, <hr/>, etc.). Also enforces self-closing syntax for void elements <br></br><br />, <img src="..."></img><img src="..." />
jsx-dom-runtime/no-legacy-event-handler Suggests using on:* event directive syntax instead of legacy on* handlers for better event management No
jsx-dom-runtime/no-spread-attribute-in-dom-element Disallows JSX spread attributes in HTML/SVG/MathML elements to maintain explicit attribute declarations No
jsx-dom-runtime/no-spread-children Disallows JSX spread children (e.g., {...items} as a child). Use the value directly instead <div>{...items}</div><div>{items}</div>
jsx-dom-runtime/prefer-attributes-over-properties Suggests using HTML attributes (class, for) over DOM properties (className, htmlFor). Use prop:* directive if you need the property instead <div className="box" /><div class="box" />, <label htmlFor="input" /><label for="input" />

TypeScript Support

This library uses TypeScript for type-checking only. For compilation, it relies on Babel. Use the @babel/preset-typescript preset to transform TypeScript files.

.babelrc

{
  "presets": [
    "@babel/preset-typescript",
    "jsx-dom-runtime/babel-preset"
  ]
}

To enable type-checking for JSX, create a tsconfig.json file in your project root. This configuration tells the TypeScript compiler how to handle JSX syntax and module resolution for this library:

tsconfig.json

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "jsx-dom-runtime",
    "moduleResolution": "node",
    "noEmit": true,
    "lib": [
      "DOM"
    ]
  }
}

Example:

src/index.tsx

import { useText } from 'jsx-dom-runtime';

interface Props {
  label: string;
}

const App: JSX.FC<Props> = ({ label }) => {
  let i = 0;

  const [textNode, setCount] = useText(i);

  const clickHandler: JSX.EventListener = () => {
    setCount(++i);
  };

  return (
    <div class="card">
      <h1 class="label">{label}</h1>
      <button type="button" on:click={clickHandler}>
        Click me! {textNode}
      </button>
    </div>
  );
};

document.body.append(<App label="Hello!" />);

License

MIT