Skip to content

directkit/just-jsx

Repository files navigation

Just JSX

GitHub Release No Dependencies License: MIT CI

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" />);

Why Just JSX?

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.

Installation

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-jsx

Quick Start

1. Configure your build tool

Tell 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"
      }
    }
  }
}

2. Start writing JSX

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".

Examples

Components with Props

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="🎉" />);

Lists and Conditionals

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} />
);

Event Handlers

const Counter = () => {
  let count = 0;
  const button = (
    <button onClick={() => {
      count++;
      button.textContent = `Count: ${count}`;
    }}>
      Count: 0
    </button>
  );
  return button;
};

document.body.appendChild(<Counter />);

DOM Refs

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);
  }} />
);

SVG Graphics

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" />);

Fragments

Use fragments to return multiple elements without a wrapper:

const Header = () => (
  <>
    <h1>Title</h1>
    <p>Subtitle</p>
  </>
);

TypeScript Support

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" />  // ✅ OK

How It Works

JSX 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 on become event listeners: onClick={fn}addEventListener('click', fn)
  • style prop 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 value and checked are set as properties, not attributes

Security

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: URLs

Defense in depth:

  1. Set Content-Security-Policy headers
  2. Validate all user input on the server
  3. Serve content over HTTPS only
  4. Use browser's built-in escaping (text content, not innerHTML)

Advanced Patterns

See the examples/recipes directory for:

Limitations

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.

API Reference

createDomElement<P>(tag, props, ...children): JSX.Element

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 handlers
  • children: Zero or more child elements (elements, strings, numbers, or arrays)

Returns: DOM Element, DocumentFragment, or primitive (string/number/boolean/null/undefined)

Special props:

  • key is filtered out (reserved for future use)
  • ref accepts callback (el) => void or object {current: T | null} for DOM access
  • Props starting with on + function value → event listeners
  • style as object → converted to CSS string
  • Boolean attributes (disabled, readonly, etc.) → removed when false
  • Specific props (value, checked, selected, innerHTML) → set as properties

createDomFragment(props, ...children): DocumentFragment

Creates a DocumentFragment (for JSX fragments <></>).

Parameters:

  • props: Unused (fragments don't have props, but can have children via spread)
  • children: Child elements to include in fragment

Returns: DocumentFragment containing all children

FunctionalComponent<P>

TypeScript type for functional components:

type FunctionalComponent<P = {}> = (
  props: P & { children?: JSX.Element | JSX.Element[] }
) => JSX.Element;

createRef<T>(): { current: T | null }

Creates a ref object for accessing DOM elements:

const myDiv = createRef<HTMLDivElement>();
<div ref={myDiv}>Hello</div>
// Later: myDiv.current?.classList.add('active')

Ref<T>

TypeScript type for refs (callback or object):

type Ref<T = Element> = ((el: T) => void) | { current: T | null };

Version Management

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.ts

See CHANGELOG.md for version history.

Performance

Just JSX is designed for direct DOM manipulation with minimal overhead. Run benchmarks locally:

pnpm install
pnpm bench

Key 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

Contributing

See CONTRIBUTING.md for development setup and guidelines.

License

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.

About

JSX syntax for vanilla TypeScript projects. Vendor directly into your codebase—no framework overhead, no supply chain dependencies.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors