Library / SDK
vadimdemedes/dom-chef avatar
vadimdemedes/dom-chef

dom-chef: JSX that builds plain DOM elements without React

🍔 Build DOM elements using JSX automatically

531 stars15 forksTypeScriptMIT

At a glance

What is it?
dom-chef is a small TypeScript library that turns JSX into ordinary DOM nodes, so you can write markup in a project with no React dependency and no innerHTML. It supports SVG, event listeners, inline styles, nested elements and component-like functions, and it is MIT licensed with the last release at v5.2.0 in July 2026.
Who is it for?
Adopt dom-chef if you want JSX syntax in a project that has no React dependency and no interest in a virtual DOM, and you already control the JSX transform. Do not adopt it if you need React semantics, since there is no mounting, no reconciliation and no lifecycle, or if your build cannot be configured with a custom JSX factory.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 98 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

JSX without the React runtime

dom-chef builds regular DOM elements using JSX, and the pitch is precise about what it avoids. With dom-chef you can use Babel or TypeScript to transform JSX into plain old DOM elements, without using the unsafe innerHTML or clumsy document.createElement calls.

The mechanism is a JSX factory. Instead of the default React.createElement, the compiler is pointed at dom-chef's h function, so the JSX you write compiles into calls that build real DOM nodes. What you get back is an actual element, which you then append somewhere: document.body.appendChild(el) in the README's example.

There is no virtual DOM and no reconciliation, because there is nothing to reconcile. The library has no concept of mounting, and the README is explicit that a ref callback is executed immediately after the element is created rather than when it is mounted, which is the single clearest sign of how differently this behaves from React.

That is the whole trade. You get declarative markup with no framework runtime and no unsafe HTML parsing, and you give up the component model, the state model and the update cycle. For a script that builds a widget once, that is a good trade. For an application whose DOM changes over time, you are back to writing imperative update code, and the JSX is only doing the initial construction.

The repository is small and self-contained: index.ts as the single source file, global.d.ts, tests/, tsconfig.json, a .npmrc, an .editorconfig and a media/ directory for the logo. The package is published as dom-chef, marked as type module, with main and module both pointing at index.js, types at index.d.ts, and only index.js and index.d.ts in the published files. The runtime dependencies are just @types/react and svg-tag-names, the latter being what lets the library know which element names are SVG tags.

Pointing Babel or TypeScript at the h factory

You need a JSX transpiler, and the README names Babel, the TypeScript compiler or esbuild, noting that you only need one of them. The configuration differs per compiler but the concept is the same: the JSX factory is dom-chef's h, and the fragment factory is DocumentFragment.

For Babel, the pragma and pragmaFrag must be configured on the transform-react-jsx plugin:

js
// babel.config.js

const plugins = [
	[
		'@babel/plugin-transform-react-jsx',
		{
			pragma: 'h',
			pragmaFrag: 'DocumentFragment',
		},
	],
];

// ...

For the TypeScript compiler, jsxFactory and jsxFragmentFactory must be configured in tsconfig.json:

jsonc
// tsconfig.json

{
	"compilerOptions": {
		"jsxFactory": "h",
		"jsxFragmentFactory": "DocumentFragment"
	}
}

There is a third route, and it is the one to use if you cannot change the compiler configuration of an existing project. You can avoid configuring your JSX compiler by letting it default to React and exporting the React object:

js
import React from 'dom-chef';

That import is what makes the default factory work, because the compiled JSX calls React.createElement and dom-chef provides an object with that name. The cost is a slightly confusing import in code that is not React at all, which is a trade most people will only make in a project they do not control.

The install itself is a single command:

bash
$ npm install dom-chef

And the resulting usage looks like this:

jsx
import {h} from 'dom-chef';

const handleClick = e => {
	// <button> was clicked
};

const el = (
	<div className="header">
		<button className="btn-link" onClick={handleClick}>
			Download
		</button>
	</div>
);

document.body.appendChild(el);

Classes, styles, listeners and innerHTML

The supported surface is listed in the README and each item has a recipe.

Classes work with either attribute name. The plain class attribute is supported, and so is the className alias, which is the React spelling:

jsx
const el = <span class="a b c">Text</span>;

// or use `className` alias
const el = <span className="a b c">Text</span>;

Inline styles take an object, the React way:

jsx
const el = <div style={{padding: 10, background: '#000'}} />;

Inline event listeners take a handler, and the README states the equivalence explicitly: this is the same as span.addEventListener('click', handleClick).

jsx
const handleClick = e => {
	// <span> was clicked
};

const el = <span onClick={handleClick}>Text</span>;

Nested elements are ordinary composition, with each child built first and then placed inside the parent:

jsx
const title = <h1>Hello World</h1>;
const body = <p>Post body</p>;

const post = (
	<div class="post">
		{title}
		{body}
	</div>
);

And the escape hatch exists, with a name that is a warning. dangerouslySetInnerHTML is supported, which is React's name for the same thing, and the example makes the risk obvious by using a script tag as the payload.

One inconsistency to note: the library's stated advantage is avoiding unsafe innerHTML, yet innerHTML is still reachable through this prop. It is opt-in, and the name signals it, but the option is there and a reader copying the recipe gets a script execution primitive.

SVG support with a list of exclusions

SVG is supported, and the example is a straightforward one:

jsx
const el = (
	<svg width={400} height={400}>
		<text x={100} y={100}>
			Wow
		</text>
	</svg>
);

The note attached to it is the part that matters. Due to the way dom-chef works, the tags a, audio, canvas, iframe, script and video aren't supported inside an svg tag.

That list is worth understanding rather than just noting. An anchor inside an SVG is a common pattern for wrapping a graphic in a link, and a script inside an SVG is how you embed interactivity, so both exclusions hit real use cases. The reason is in the mechanism: the library creates elements with a namespace-aware path, and the tags on that list are ones that either create a separate browsing context or behave differently when created in the SVG namespace, so the library declines to create them there.

The dependency list explains part of the story. svg-tag-names is a runtime dependency, so the library ships a list of known SVG tag names and uses it to decide which namespace to create an element in. That is why the exclusions are a fixed list rather than a general rule, and it is why a package this small still needs a runtime dependency at all.

If your SVG needs a link or a script inside it, dom-chef is the wrong tool for that element and you would fall back to document.createElementNS for those specific nodes.

Function elements and the defaultProps behaviour

The most interesting feature is the one that has no React equivalent at all.

If element names start with an uppercase letter, dom-chef treats them as element-returning functions. So you can define a function that builds an element and call it as if it were a component:

jsx
function Title() {
	const title = document.createElement('h1');
	title.classList.add('Heading');
	return title;
}

const el = <Title className="red">La Divina Commedia</Title>;
// <h1 class="Heading red">La Divina Commedia</h1>

The README explains why this is useful. It makes JSX a good way to apply multiple attributes and content at once, and the second example shows the pattern with a function that clones an existing SVG node:

jsx
const BaseIcon = () => document.querySelector('svg.icon').cloneNode(true);

document.body.append(
	<BaseIcon width="16" title="Starry Day" className="margin-0" />
);

There is also a compatibility gesture towards React component authors. dom-chef will pass the function's defaultProps property to itself if present, and the README is careful about the semantics: specifying attributes won't override those defaults, but instead set them on the resulting element. The example shows an AlertIcon with defaultProps of className icon icon-alert and size 16, and passing className margin-0 and size 32 produces an svg with width 16, class icon icon-alert margin-0 and size 32, so the default wins on the conflicting fields and the explicit value is appended.

That is a slightly odd but workable rule, and it exists so that a React component can be reused here without rewriting it.

Small surface, fast-moving versions, MIT terms

The package is MIT licensed, with the copyright held by Vadim Demedes and the licence file at the repository root. The package is published as dom-chef at version 5.2.0, with Federico Brigante listed as a maintainer.

The release history shows the API settling over time. The listed versions are v5.2.0 on 2026-07-17, v5.1.1 on 2023-12-15 and v5.1.0 on 2022-03-23. Two and a half years between 5.1.1 and 5.2.0 says as much about the project's pace as the version numbers do, and a major version here means the JSX factory contract or the supported attribute surface may have moved.

The last push was on 2026-07-17, the same day as the 5.2.0 release, so the tree is current with the published package. The project is not archived, and the README asks for issues if something is not supported or does not work as well as it does in React, which is a fair framing for a library whose whole purpose is to sit close to React's ergonomics.

The engineering setup is proportionate to the size of the thing. The build is tsc, the test command runs xo and ava, and the xo configuration carries a long list of disabled rules, including unicorn/prefer-dom-node-append and a set of typescript-eslint rules turned off for the unsafe-anything family. The xo plugins list includes react with react/jsx-uses-vars and react/jsx-uses-react set to error, which is what lets a project without React still lint JSX.

A repository this small is easy to read end to end, which is a real advantage over a larger alternative that does the same job.

Editorial conclusion

Adopt dom-chef if you want JSX syntax in a project that has no React dependency and no interest in a virtual DOM, and you already control the JSX transform. Do not adopt it if you need React semantics, since there is no mounting, no reconciliation and no lifecycle, or if your build cannot be configured with a custom JSX factory. Verify first that the pragma and pragmaFrag settings match the h export in every compiler you use, that the SVG tag restrictions do not affect the elements you need, and that the version you install is v5.2.0 or later, since the API moved across major versions.

Frequently asked questions

What does dom-chef do and how is it different from React?

dom-chef transforms JSX into plain DOM elements rather than React components, so you build regular DOM nodes without a framework runtime, without innerHTML and without document.createElement calls. It has no concept of mounting, and a ref callback fires immediately after the element is created.

How do I install and configure dom-chef?

Run npm install dom-chef, then point your JSX compiler at its factory. For Babel set pragma to h and pragmaFrag to DocumentFragment on the transform-react-jsx plugin, and for TypeScript set jsxFactory to h and jsxFragmentFactory to DocumentFragment in tsconfig.json.

Which HTML elements are not supported inside an SVG in dom-chef?

The README notes that the tags a, audio, canvas, iframe, script and video aren't supported inside an svg tag, because of the way dom-chef creates elements. Elements with those names inside an SVG need another approach.

How do function elements work in dom-chef?

If an element name starts with an uppercase letter, dom-chef treats it as a function that returns an element, so a function that builds a DOM node can be used as if it were a component. If the function has a defaultProps property, those defaults are used, and attributes you pass are set on the result rather than overriding the defaults.

What licence is dom-chef released under?

MIT, with the copyright held by Vadim Demedes and the licence file at the root of the repository. The package is published as dom-chef at version 5.2.0, and the previous listed releases are v5.1.1 from 2023-12-15 and v5.1.0 from 2022-03-23.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. vadimdemedes/dom-chef on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/vadimdemedes-dom-chef.svg)](https://hysenlabs.com/projects/vadimdemedes-dom-chef)

Was this useful?