-
Notifications
You must be signed in to change notification settings - Fork 2
Home
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.
npm i jsx-dom-runtime
# or
yarn add jsx-dom-runtime
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"
]
}
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.
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 ofhtmlFor
:
- <label htmlFor="cheese">Do you like cheese?</label>
+ <label for="cheese">Do you like cheese?</label>
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' }} />
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>
There are a few ways to add event handling to a DOM Element.
- Using the event handler properties that start with
on*
asonclick
orondblclick
.
<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) => { };
- Using the namespace syntax for the event listener that start with
on:*
ason:change
oron: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) => { });
- 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>;
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
andundefined
values are ignored - Objects are converted using
toString()
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
orinnerHTML
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 must start with a capital letter or they won’t work.
const App = ({ name }) => (
<div>Hello {name}</div>
);
document.body.append(<App name="Bob" />);
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>
</>
);
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} />
</>
);
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} />
);
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>
</>
);
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>
);
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 {};
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" />
|
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!" />);