JSX syntax for vanilla TypeScript projects. A ~223-line library you vendor directly into your codebase—no framework overhead, no supply chain dependencies, small enough to audit in 10 minutes.
const App = ({ name }) => (
<div>
<h1>Hello, {name}!</h1>
<button onClick={() => alert('Clicked!')}>
Click me
</button>
</div>
);
document.body.appendChild(<App name="World" />);Just JSX gives you:
- ✅ Familiar JSX syntax for building UIs
- ✅ Direct DOM manipulation (no virtual DOM overhead)
- ✅ Full TypeScript support with proper type inference
- ✅ SVG elements work out of the box
- ✅ Small enough to audit in 10 minutes (~223 lines)
- ✅ No build-time or runtime dependencies
Vendor it into your project. Read it, understand it, modify it as needed.
Pre-built bundles (no build step required):
Download IIFE or UMD from releases
<script src="./just-jsx.iife.min.js"></script>
<script>
const { createDomElement } = window.JustJSX;
</script>TypeScript source:
Download source from releases and import directly
Vendor with git:
git submodule add https://github.com/ge3224/just-jsx.git vendor/just-jsxTell your compiler to use Just JSX for JSX transformation:
TypeScript (tsconfig.json)
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "createDomElement",
"jsxFragmentFactory": "createDomFragment"
}
}Vite (vite.config.ts)
export default {
esbuild: {
jsxFactory: "createDomElement",
jsxFragment: "createDomFragment"
}
};SWC (.swcrc)
{
"jsc": {
"transform": {
"react": {
"pragma": "createDomElement",
"pragmaFrag": "createDomFragment"
}
}
}
}import { createDomElement, createDomFragment } from './vendor/just-jsx';
const greeting = <h1>Hello JSX!</h1>;
document.body.appendChild(greeting);Note: The package exports configuration allows you to import directly from './vendor/just-jsx' instead of './vendor/just-jsx/src' when using un-compiled TypeScript with modern bundlers or TypeScript's moduleResolution: "bundler".
type GreetingProps = { name: string; emoji?: string };
const Greeting = ({ name, emoji = '👋' }: GreetingProps) => (
<div class="greeting">
<h2>{emoji} Hi, {name}!</h2>
</div>
);
document.body.appendChild(<Greeting name="Alice" emoji="🎉" />);const TodoList = ({ items, showCompleted }) => (
<ul>
{items
.filter(item => showCompleted || !item.done)
.map(item => (
<li class={item.done ? 'completed' : ''}>
{item.text}
</li>
))}
</ul>
);
const todos = [
{ text: 'Learn JSX', done: true },
{ text: 'Build something', done: false }
];
document.body.appendChild(
<TodoList items={todos} showCompleted={true} />
);const Counter = () => {
let count = 0;
const button = (
<button onClick={() => {
count++;
button.textContent = `Count: ${count}`;
}}>
Count: 0
</button>
);
return button;
};
document.body.appendChild(<Counter />);Access DOM elements directly using refs:
import { createRef } from './vendor/just-jsx';
// Object form (like React)
const input = createRef<HTMLInputElement>();
const button = (
<button onClick={() => input.current?.focus()}>
Focus Input
</button>
);
document.body.appendChild(
<>
<input ref={input} type="text" />
{button}
</>
);
// Callback form (like SolidJS)
const canvas = (
<canvas ref={(el) => {
const ctx = el.getContext('2d');
ctx.fillRect(0, 0, 100, 100);
}} />
);const Icon = ({ size = 24, color = 'currentColor' }) => (
<svg width={size} height={size} viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" fill={color} />
<path d="M12 6v6l4 4" stroke="white" stroke-width="2" />
</svg>
);
document.body.appendChild(<Icon size={48} color="blue" />);Use fragments to return multiple elements without a wrapper:
const Header = () => (
<>
<h1>Title</h1>
<p>Subtitle</p>
</>
);Full type safety with autocomplete for all HTML and SVG elements:
import type { FunctionalComponent } from './vendor/just-jsx';
// Typed component props
type ButtonProps = {
label: string;
variant?: 'primary' | 'secondary';
onClick?: () => void;
};
const Button: FunctionalComponent<ButtonProps> = ({
label,
variant = 'primary',
onClick
}) => (
<button
class={`btn btn-${variant}`}
onClick={onClick}
>
{label}
</button>
);
// Type errors caught at compile time
<Button label="Click" variant="invalid" /> // ❌ Error
<Button label="Click" variant="primary" /> // ✅ OKJSX is syntactic sugar. Build tools transform it into function calls:
// You write:
<div class="box">
<span>Hello</span>
</div>
// Build tool transforms to:
createDomElement('div', { class: 'box' },
createDomElement('span', null, 'Hello')
)
// Just JSX executes:
const div = document.createElement('div');
div.setAttribute('class', 'box');
const span = document.createElement('span');
span.textContent = 'Hello';
div.appendChild(span);
return div;Key behaviors:
- Props starting with
onbecome event listeners:onClick={fn}→addEventListener('click', fn) styleprop accepts objects:style={{ color: 'red' }}→style="color: red"- Boolean attributes work correctly:
disabled={false}removes the attribute - SVG elements use the correct XML namespace automatically
- Form properties like
valueandcheckedare set as properties, not attributes
Just JSX does not sanitize input. Follow these guidelines:
✅ Safe patterns:
// Text content is auto-escaped by the browser
<div>{userInput}</div>
// Use textContent for plain text
<div textContent={userInput} />
// Validate URLs before rendering
const isValidUrl = url.startsWith('https://') || url.startsWith('/');
<a href={isValidUrl ? url : '#'}>{text}</a>❌ Unsafe patterns:
// Never use innerHTML with untrusted content
<div innerHTML={userProvidedHtml} />
// Always validate href attributes
<a href={userInput}>Link</a> // Potential javascript: URLsDefense in depth:
- Set Content-Security-Policy headers
- Validate all user input on the server
- Serve content over HTTPS only
- Use browser's built-in escaping (text content, not innerHTML)
See the examples/recipes directory for:
- Memory Management - Cleaning up event listeners and timers
Just JSX is intentionally minimal. It does not provide:
- ❌ State management (use vanilla JS or Simple State)
- ❌ Component lifecycle hooks (use vanilla patterns)
- ❌ Virtual DOM diffing (direct DOM updates only)
- ❌ Server-side rendering
- ❌ Hot module replacement
- ❌ React compatibility layer
If you need these features, use React. Just JSX is for projects where simplicity and control matter more than ecosystem features.
Creates a DOM element or invokes a functional component.
Parameters:
tag: HTML/SVG tag name (string) or functional component (function)props: Object with attributes, properties, and event handlerschildren: Zero or more child elements (elements, strings, numbers, or arrays)
Returns: DOM Element, DocumentFragment, or primitive (string/number/boolean/null/undefined)
Special props:
keyis filtered out (reserved for future use)refaccepts callback(el) => voidor object{current: T | null}for DOM access- Props starting with
on+ function value → event listeners styleas object → converted to CSS string- Boolean attributes (
disabled,readonly, etc.) → removed whenfalse - Specific props (
value,checked,selected,innerHTML) → set as properties
Creates a DocumentFragment (for JSX fragments <></>).
Parameters:
props: Unused (fragments don't have props, but can havechildrenvia spread)children: Child elements to include in fragment
Returns: DocumentFragment containing all children
TypeScript type for functional components:
type FunctionalComponent<P = {}> = (
props: P & { children?: JSX.Element | JSX.Element[] }
) => JSX.Element;Creates a ref object for accessing DOM elements:
const myDiv = createRef<HTMLDivElement>();
<div ref={myDiv}>Hello</div>
// Later: myDiv.current?.classList.add('active')TypeScript type for refs (callback or object):
type Ref<T = Element> = ((el: T) => void) | { current: T | null };Track versions using git tags:
# View available versions
git tag
# Use specific version (submodule)
cd vendor/just-jsx
git fetch --tags
git checkout v0.1.10
cd ../..
git add vendor/just-jsx
git commit -m "Upgrade just-jsx to v0.1.10"
# Use specific version (direct copy)
curl -o src/jsx.ts https://raw.githubusercontent.com/ge3224/just-jsx/v0.1.6/src/index.tsSee CHANGELOG.md for version history.
Just JSX is designed for direct DOM manipulation with minimal overhead. Run benchmarks locally:
pnpm install
pnpm benchKey performance characteristics:
- Element creation: ~183k ops/sec for simple elements
- List rendering: ~8.4k ops/sec for 10 items, ~922 ops/sec for 100 items
- SVG rendering: ~40k ops/sec for simple SVG elements
- Comparison to vanilla: ~7-10% overhead vs raw
document.createElement
The overhead comes from JSX conveniences (prop processing, event listeners, style objects). For performance-critical sections, you can always drop down to vanilla DOM.
See benchmark/index.bench.tsx for detailed benchmarks including:
- Basic operations (create, props, children, fragments)
- Scaling tests (lists, nesting, wide children)
- Real-world scenarios (todo lists, tables, forms, card grids)
- SVG rendering
- Comparison with vanilla DOM
See CONTRIBUTING.md for development setup and guidelines.
MIT © Jacob Benison
Not on npm by design. Just JSX is meant to be vendored (copied into your codebase). This gives you:
- Full control over updates
- No supply chain vulnerabilities
- Easy auditing (just read the file)
- Zero installation friction
Copy the code, read it, modify it, own it.