npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

npm jsr License: MIT

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

Usage

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