@lilian1315/create-element
v0.14.1
Published
Type-safe DOM and virtual element creation with JSX, SSR, and optional reactive integrations.
Readme
@lilian1315/create-element
Type-safe DOM and virtual element creation with JSX, server rendering, and optional reactive library integrations.
Features
- Type-safe element creation with full TypeScript autocompletion (Usage)
- Support for HTML, SVG, and MathML elements (SVG and MathML)
- Flexible attribute handling: classes, styles, datasets, events (Attributes)
- JSX support with Fragments and function components (JSX Support)
- Lightweight virtual trees and server rendering (Virtual Trees and SSR)
- Reactive adapters for signal-based UI updates (Reactive Support)
Installation
pnpm add @lilian1315/create-elementUsage
import { h } from '@lilian1315/create-element'
// Create a simple element
const div = h('div', { class: 'container' }, 'Hello World!')
// With event handlers
const button = h('button', {
onclick: () => console.log('Clicked!'),
children: 'Click me',
})
// Nested elements
const app = h('div', null, [h('h1', null, 'My App'), button])API
h(tag, attributes?, ...children)
Creates a DOM element and returns it.
| Parameter | Type | Description |
| ------------ | ---------------- | ---------------------------------------------------------------------------------------------------- |
| tag | string | HTML tag name (e.g. 'div'), or prefixed for SVG/MathML ('svg:circle', 'math:mi') |
| attributes | object \| null | Optional attribute bag including special helpers (class, style, data, innerHTML, children) |
| children | Child[] | Additional child nodes — strings, numbers, DOM nodes, null, or arrays thereof |
Returns: the created DOM element (HTMLElement, SVGElement, or MathMLElement).
Attributes
// Classes — string, array, or conditional object
h('div', { class: 'btn primary' })
h('div', { class: ['btn', 'primary'] })
h('div', { class: { btn: true, primary: true, active: false } })
// Styles — string or object
h('div', { style: 'color: red' })
h('div', { style: { color: 'red', fontSize: '16px' } })
// Events — lowercase on* handlers
h('button', { onclick: () => console.log('clicked') })
// Data attributes — mapped to element.dataset
h('div', {
data: {
testId: 'my-component', // data-test-id="my-component"
active: true, // data-active=""
hidden: false, // removed
count: null, // removed
empty: undefined, // removed
},
})
// innerHTML (mutually exclusive with children)
h('div', { innerHTML: '<span>content</span>' })SVG and MathML
Use svg: or math: prefixes for namespace-aware element creation:
// SVG elements
const svg = h('svg', { width: '100', height: '100' })
const circle = h('svg:circle', { cx: '50', cy: '50', r: '20' })
// MathML elements
const math = h('math')
const variable = h('math:mi', null, 'x')The root svg and math tags do not need a prefix.
JSX Support
Configure TypeScript to use the JSX runtime:
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@lilian1315/create-element",
},
}Elements and function components
function Greeting({ name }: { name: string }) {
return <h1>Hello, {name}!</h1>
}
function App() {
return (
<div class="container">
<Greeting name="World" />
<button onclick={() => console.log('clicked')}>Click me</button>
</div>
)
}
document.body.appendChild(App())Fragments
Use Fragment to group children without an extra DOM wrapper. A Fragment returns a Node[].
import { Fragment } from '@lilian1315/create-element/jsx-runtime'
function List() {
return (
<>
<li>One</li>
<li>Two</li>
</>
)
}Virtual Trees and SSR
The /virtual entry point uses the same h(type, props, ...children) API but returns lightweight
Preact-style VNodes instead of creating DOM elements immediately. mount materializes a tree in
the browser, while the separate /server entry point serializes the same tree without requiring a DOM.
This is a virtual tree API, not a reconciler: mounting materializes the complete tree, and VNode keys
are currently metadata only.
import { h, mount } from '@lilian1315/create-element/virtual'
import { renderToString } from '@lilian1315/create-element/server'
const app = h('main', { class: 'page' }, h('h1', null, 'Hello from a VNode'))
const html = renderToString(app)
// <main class="page"><h1>Hello from a VNode</h1></main>
mount(document.querySelector<HTMLElement>('#app')!, app)For virtual JSX, use the virtual runtime:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@lilian1315/create-element/virtual",
},
}renderToString is synchronous and produces static HTML. Event handlers are omitted. innerHTML
is emitted without escaping and must only receive trusted or sanitized content. Client hydration is
not currently included.
createElementFromVNode materializes one intrinsic-element VNode. Components and fragments can
produce several nodes, so pass those trees to mount instead.
Reactive server rendering
Append /virtual to an adapter path to create reactive VNodes, then use the corresponding /server
renderer to serialize their current values:
import { shallowRef } from '@vue/reactivity'
import { renderToString } from '@lilian1315/create-element/vue-reactivity/server'
import { h, mount } from '@lilian1315/create-element/vue-reactivity/virtual'
const count = shallowRef(1)
const tree = h('p', null, 'Count: ', count)
renderToString(tree) // <p>Count: 1</p>
const dispose = mount(document.querySelector<HTMLElement>('#app')!, tree)
dispose()The same /virtual and /server pair is available for alien-signals, alien-deepsignals,
faisceau, preact-signals, and vue-reactivity. Reactive mount subscribes to reactive values and
returns a disposer that stops every owned effect, removes event listeners, and clears the target.
Mounting again into the same target disposes its previous tree automatically. Server rendering reads
sources with peek, produces one static snapshot, and does not subscribe to later updates. For
reactive VNode JSX, use the adapter's /virtual path as jsxImportSource.
Reactive JSX
For reactive JSX, set jsxImportSource to the adapter path:
| Signal library | jsxImportSource |
| -------------------- | ---------------------------------------------- |
| alien-signals | @lilian1315/create-element/alien-signals |
| alien-deepsignals | @lilian1315/create-element/alien-deepsignals |
| faisceau | @lilian1315/create-element/faisceau |
| @preact/signals-core | @lilian1315/create-element/preact-signals |
| @vue/reactivity | @lilian1315/create-element/vue-reactivity |
Precise JSX element types
TypeScript assigns JSX.Element to every JSX expression, even when this runtime creates a more
specific DOM element. Use asDom to state the concrete type:
import { asDom } from '@lilian1315/create-element'
const container = asDom<'div'>(<div />)
const path = asDom<'svg:path'>(<svg:path />)The accompanying Oxlint plugin verifies that the explicit type argument matches the intrinsic JSX
tag. It also checks direct assertions such as <div /> as HTMLDivElement and can fix mismatches.
{
"jsPlugins": ["@lilian1315/oxlint-plugin-create-element"],
"rules": {
"create-element/valid-jsx-type-assertion": "error"
}
}The rule is syntax-based. Type aliases and qualified type names are ignored because Oxlint JS plugins do not expose TypeScript type information.
Reactive Support (Optional)
Each reactive adapter wraps createElement so that signal/computed values in attributes, styles, datasets, and children are automatically tracked and updated in the DOM.
Import h from the adapter that matches your signal library and install the corresponding peer dependency.
alien-signals
import { h } from '@lilian1315/create-element/alien-signals'
import { computed, signal } from 'alien-signals'
const count = signal(0)
const label = computed(() => `Count: ${count()}`)
const counter = h('section', { class: 'counter' }, [
h('p', null, label),
h('button', { onclick: () => count(count() + 1) }, 'Increment'),
])Requires: pnpm add alien-signals
alien-deepsignals
import { h } from '@lilian1315/create-element/alien-deepsignals'
import { computed, signal } from 'alien-deepsignals'
const count = signal(0)
const label = computed(() => `Count: ${count.get()}`)
const counter = h('section', null, [
h('p', null, label),
h('button', { onclick: () => count.set(count.get() + 1) }, 'Increment'),
])Requires: pnpm add alien-deepsignals
faisceau
import { h } from '@lilian1315/create-element/faisceau'
import { computed, signal } from 'faisceau'
const count = signal(0)
const label = computed(() => `Count: ${count.get()}`)
const counter = h('section', null, [
h('p', null, label),
h('button', { onclick: () => count.set(count.get() + 1) }, 'Increment'),
])Requires: pnpm add faisceau
@preact/signals-core
import { h } from '@lilian1315/create-element/preact-signals'
import { computed, signal } from '@preact/signals-core'
const count = signal(0)
const label = computed(() => `Count: ${count.value}`)
const counter = h('section', null, [
h('p', null, label),
h('button', { onclick: () => (count.value = count.value + 1) }, 'Increment'),
])Requires: pnpm add @preact/signals-core
@vue/reactivity
import { h } from '@lilian1315/create-element/vue-reactivity'
import { computed, ref } from '@vue/reactivity'
const count = ref(0)
const label = computed(() => `Count: ${count.value}`)
const counter = h('section', null, [
h('p', null, label),
h('button', { onclick: () => count.value++ }, 'Increment'),
])Requires: pnpm add @vue/reactivity
License
MIT
