# jsx-dom-runtime

This is a [Babel](https://babeljs.io/) plugin to transform [JSX](https://facebook.github.io/jsx/) syntax to [DOM](https://dom.spec.whatwg.org/) elements with minimal runtime dependency ~500 B. It supports [HTML](https://html.spec.whatwg.org/multipage/), [SVG](https://www.w3.org/TR/SVG/), [MathML](https://www.w3.org/TR/MathML3/) and [Custom elements](https://html.spec.whatwg.org/multipage/custom-elements.html) tags.

**source code:**

```js
import { render } from 'jsx-dom-runtime';

render(
  <main class="box">
    <h1 class="title">Hello, World!</h1>
  </main>,
  document.getElementById('root')
);
```

**after compilation:**

```js
import { jsx as _jsx } from "jsx-dom-runtime";
import { render } from 'jsx-dom-runtime';

render(
  _jsx("main", {
    class: "box"
  }, _jsx("h1", {
    class: "title"
  }, "Hello, World!")),
  document.getElementById("root")
);
```

The Babel preset injects the JSX runtime functions automatically. Import `render` explicitly to mount the resulting nodes.

## Install

```bash
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](https://babeljs.io/docs/en/config-files) (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**

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

### Vite

[Vite v7](https://v7.vite.dev/) and [Vite v8](https://vite.dev/) use different transformers and plugin systems. Choose the configuration that matches your Vite version.

Vite v7 uses [esbuild](https://esbuild.github.io/api/) and [Rollup](https://rollupjs.org/introduction/). Configure esbuild to preserve JSX so that Babel can transform it with this library's preset.

**vite.config.ts**

```ts
import { defineConfig } from 'vite';
import babel from '@rollup/plugin-babel';

export default defineConfig(() => {
  return {
    // Vite config ...
    esbuild: {
      // Preserve JSX syntax in esbuild's output instead of transforming it.
      // This allows Babel to process the JSX later in the build pipeline.
      jsx: 'preserve',
    },
    plugins: [
      babel({
        babelHelpers: 'bundled',
        // Optimize the build by running Babel only on files that can contain JSX.
        extensions: ['.jsx', '.tsx'],
        presets: ['jsx-dom-runtime/babel-preset'],
      }),
    ],
  };
});
```

Vite v8 uses [Oxc](https://oxc.rs/docs/guide/what-is-oxc.html) and [Rolldown](https://rolldown.rs/). Configure Oxc to preserve JSX and use the Rolldown Babel plugin to transform it.

**vite.config.ts**

```ts
import { defineConfig } from 'vite';
import babel from '@rolldown/plugin-babel';

export default defineConfig(() => {
  return {
    // Vite config ...
    oxc: {
      // Preserve JSX syntax in Oxc's output instead of transforming it.
      // This allows Babel to process the JSX later in the build pipeline.
      jsx: 'preserve',
      target: 'esnext',
    },
    plugins: [
      babel({
        // Optimize the build by running Babel only on files that can contain JSX.
        include: /\.[jt]sx$/,
        presets: ['jsx-dom-runtime/babel-preset'],
      }),
    ],
  };
});
```

### Webpack

Use [Webpack](https://webpack.js.org/) with [`babel-loader`](https://github.com/babel/babel-loader) and apply the preset to your TS/JSX files.

**webpack.config.js**

```js
module.exports = {
  // Webpack config ...
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          presets: ['jsx-dom-runtime/babel-preset'],
        },
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },
};
```

### Rollup

Use [Rollup](https://rollupjs.org/) with [`@rollup/plugin-babel`](https://github.com/rollup/plugins/tree/master/packages/babel) to apply this Babel plugin.

```js
import { babel } from '@rollup/plugin-babel';

export default {
  // Rollup config ...
  plugins: [
    babel({
      babelHelpers: 'bundled',
      extensions: ['.js', '.jsx', '.ts', '.tsx'],
      presets: ['jsx-dom-runtime/babel-preset'],
    }),
  ],
}
```

### Rolldown

Use [Rolldown](https://rolldown.rs/) with [`@rolldown/plugin-babel`](https://www.npmjs.com/package/@rolldown/plugin-babel). Preserve JSX during Rolldown's transform step so that Babel can transform it with this library's preset.

**rolldown.config.js**

```js
import { defineConfig } from 'rolldown';
import babel from '@rolldown/plugin-babel';

export default defineConfig({
  // Rolldown config ...
  transform: {
    // Preserve JSX syntax in Oxc's output instead of transforming it.
    // This allows Babel to process the JSX later in the build pipeline.
    jsx: 'preserve',
  },
  plugins: [
    babel({
      // Optimize the build by running Babel only on files that can contain JSX.
      include: /\.[jt]sx$/,
      presets: ['jsx-dom-runtime/babel-preset'],
    }),
  ],
});
```

### Parcel

[Parcel](https://parceljs.org/) automatically detects Babel configuration files. Add the JSX DOM Runtime preset to a `.babelrc` file in your project root:

**.babelrc**

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

## Syntax

This library supports the standard [JSX syntax](https://facebook.github.io/jsx/), 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.

```diff
- <div className="box" />
+ <div class="box" />
```

- Use `for` instead of `htmlFor`:
```diff
- <label htmlFor="cheese">Do you like cheese?</label>
+ <label for="cheese">Do you like cheese?</label>
```

### SVG

Use unmodified SVG attributes instead of camelCase style as in React

```diff
<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`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href) you should use `href`

```diff
<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>
```

### Style

The `style` attribute supports a JavaScript object and a [string value](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/style). You can also use [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties)

```js
<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' }} />
```

### Spread Props

Spread attributes (`{...props}`) are **not supported** on DOM elements (HTML, SVG, MathML, and custom elements). This is a design decision to keep the generated code optimal and predictable.

The library generates static, compile-time code that doesn't support dynamic attribute spreading, as it would break the generated output. Spread attributes would conflict with the library's special features such as:

- Event handling directives (`on:click`, `on:change`, etc.)
- Property directives (`prop:*`)
- Attribute directives (`attr:*`)
- Ref callbacks and ref objects
- Special attributes like `style`, `dataset`, and `attributes`

These features require compile-time transformation and cannot work with runtime spread operations.
```js
const props = { class: 'box', id: 'main' };

// ❌ SyntaxError: HTML, SVG, MathML or Custom Elements must not have spread attributes.
<div {...props} />;
// ❌ SyntaxError: HTML, SVG, MathML or Custom Elements must not have spread attributes.
<svg {...props}></svg>;
// ❌ SyntaxError: HTML, SVG, MathML or Custom Elements must not have spread attributes.
<math {...props}><mi>x</mi></math>;
// ❌ SyntaxError: HTML, SVG, MathML or Custom Elements must not have spread attributes.
<my-component {...props} />;
```

Spread props are fully supported on function components because they are just JavaScript functions:

```js
// ✅ This works fine - function components are just functions

const MyComponent = ({ content, ...props }) => (
  <div class={props.class} id={props.id}>{content}</div>
);

const props = { class: 'box', id: 'main' };

<MyComponent content="Hello" {...props} />;
```

Since function components are regular JavaScript functions, the spread operator works naturally as a function argument, allowing you to pass multiple properties at once.

### Event handling

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

1. Using the [event handler properties](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_handler_properties) that start with `on*` such as `onclick` or `ondblclick`.

```js
<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.

```js
// Equivalent on vanilla JavaScript
button.onclick = (event) => { };
button.ondblclick = (event) => { };
```

2. Using the namespace syntax for event listeners that starts with `on:*` such as `on:change` or `on:focus`.

```js
<input
  type="text"
  on:change={(event) => { }}
  on:focus={(event) => { }}
/>
```

After the compilation, it registers the event with [`addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)

```js
// Equivalent on vanilla JavaScript
input.addEventListener('change', (event) => { });
input.addEventListener('focus', (event) => { });
```

3. Using `ref` callback. The callback will be called with the target element when it is created.

```js
<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.

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

The `attr:*` directive uses [`setAttribute()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute) to set attributes on the DOM element:

```js
// 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

```js
// 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.

```js
<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:

```js
// 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

```js
// 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.

```js
import { render } from 'jsx-dom-runtime';

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

render(<App name="Bob" />, document.getElementById('root'));
```

### Fragments

Use `<>...</>` to group elements. An empty fragment compiles to `null`, a fragment with one child compiles to that child, and a fragment with multiple children compiles to a flattened array.

```js
import { render } from 'jsx-dom-runtime';

render(
  <>
    <p>Hello</p>
    <p>World</p>
  </>,
  document.getElementById('root')
);
```

After compilation:

```js
import { jsx as _jsx } from 'jsx-dom-runtime';
import { render } from 'jsx-dom-runtime';

render(
  [
    _jsx('p', {}, 'Hello'),
    _jsx('p', {}, 'World')
  ],
  document.getElementById('root')
);
```

## APIs

### Creating Refs

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

```js
import { render, useRef } from 'jsx-dom-runtime';

const ref = useRef();

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

render(
  <>
    <button type="button" on:click={addItem}>
      Add Item
    </button>
    <ul ref={ref} />
  </>,
  document.getElementById('root')
);
```

### Callback Refs

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

```js
import { render } from 'jsx-dom-runtime';

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

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

render(
  <input type="text" ref={setRef} />,
  document.getElementById('root')
);
```

### Text

Use the [Text](https://developer.mozilla.org/en-US/docs/Web/API/Text) node in a DOM tree.

```js
import { render, useText } from 'jsx-dom-runtime';

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

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

render(
  <>
    <p>{text}</p>
    <button type="button" on:click={clickHandler}>
      Click me
    </button>
  </>,
  document.getElementById('root')
);
```

### signal()

A signal is a reactive value. When the value changes, every place that depends on it — an attribute, a text node, or a manual subscriber — updates automatically.

```js
import { signal } from 'jsx-dom-runtime';

const count = signal(0);

count.get();  // 0
count.set(1);
count.get();  // 1
```

**API**

| Method | Description |
|--------|-------------|
| `get()` | Returns the current value (readonly) |
| `set(val)` | Updates the value and notifies all subscribers |
| `on(fn)` | Subscribes `fn` — calls it immediately with the current value, then on every `set()`. Returns an unsubscribe function |

```js
const s = signal('hello');

const off = s.on((value) => {
  // called immediately: "hello"
  // then on every s.set(...)
  console.log(value); 
});

s.set('world'); // logs "world"

off();          // unsubscribe
s.set('!');     // nothing logged
```

**Using signals in JSX**

Pass a signal anywhere the attribute accepts `Signalish<T>` — the element updates in place whenever the signal changes.

```js
import { render, signal } from 'jsx-dom-runtime';

const label = signal('Submit');
const disabled = signal(false);

render(
  <button type="button" class="btn" prop:disabled={disabled}>
    {label}
  </button>,
  document.getElementById('root')
);

// Later — no DOM query needed
label.set('Saving…');
disabled.set(true);
```

**HTML attributes** — updates the attribute via `setAttribute`:

```js
const status = signal('idle');

<div attr:data-status={status} />;

status.set('loading'); // → data-status="loading"
```

**DOM properties** — updates the property directly:

```js
const title = signal('Hello');

<h1 prop:textContent={title} />;

title.set('World'); // → h1.textContent = "World"
```

**Text content** — pass a signal as a child:

```js
const name = signal('Alice');

<p>Hello, {name}!</p>;

name.set('Bob'); // → "Hello, Bob!"
```

**Standard attributes** — attributes typed as `Signalish<T>` accept signals directly:

```js
const cls = signal('btn');
const href = signal('/home');
const max = signal(100);

<button class={cls} />;
<a href={href} />;
<progress max={max} />;
```

**Multiple elements** sharing a signal all update together:

```js
const theme = signal('light');

const header = <header class={theme} />;
const footer = <footer class={theme} />;

theme.set('dark');
// both header and footer now have class="dark"
```

## ESLint Support

This library provides [ESLint](https://eslint.org) 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](https://eslint.org/docs/latest/use/configure/configuration-files)):


**Option 1: Basic Configuration**

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

**eslint.config.js**

```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**

```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](https://developer.mozilla.org/en-US/docs/Glossary/Void_element) (`<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`](https://babeljs.io/docs/babel-preset-typescript) preset to transform TypeScript files.

**.babelrc**

```json
{
  "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**

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

Example:

**src/index.tsx**

```ts
import { render, signal } from 'jsx-dom-runtime';

interface Props {
  label: string;
}

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

  const clickHandler: JSX.EventListener = () => {
    count.set(count.get() + 1)
  };

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

render(
  <App label="Hello!" />,
  document.getElementById('root')!
);
```

## License

[MIT](https://github.com/shoonia/jsx-dom-runtime/blob/master/LICENSE)
