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

@jdlanglois/els

v1.4.0

Published

Ultra-lightweight stream-based DOM library with keyed lists and routing

Readme

@jdlanglois/els

A lightweight, stream-based DOM library that combines the simplicity of RE:DOM's el utility with the reactive power of Mithril Streams.

  • ⚡️ Direct DOM: No virtual DOM overhead.
  • 🔄 Reactive: Attributes, styles, and text nodes bind directly to streams.
  • 🧹 Auto-Cleanup: Subscriptions are tracked and automatically ended when elements are removed.
  • 🎯 Simple API: Familiar el(selector, props, ...children) pattern.

Quick Start

bun add @jdlanglois/els mithril

1. Creating Elements

The core of the library is the el function. It uses a CSS-like selector to create real DOM nodes.

import { el, mount } from '@jdlanglois/els';

// Create a simple card
const card = el('div.card#main', 
  { title: 'Hover me' }, 
  el('h1', 'Hello World'),
  el('p', 'This is a standard DOM element.')
);

mount(document.body, card);

2. Adding Reactivity

Instead of manual DOM updates, use Streams. When a stream's value changes, any part of the DOM bound to it updates automatically.

import { el, stream } from '@jdlanglois/els';

const count = stream(0);

const counter = el('div',
  el('p', 'Count: ', count), // Text updates automatically
  el('button', { 
    onclick: () => count(count() + 1),
    style: { color: count.map(c => c > 10 ? 'red' : 'black') } // Styles update too!
  }, 'Increment')
);

3. Derived State with computed

Use computed to create values that depend on other streams. It automatically tracks dependencies—no manual dependency arrays needed.

import { stream, computed } from '@jdlanglois/els';

const first = stream('John');
const last = stream('Doe');

// Automatically reacts when either stream changes
const full = computed(() => `${first()} ${last()}`);

const greeting = el('h1', 'Hello, ', full);

4. Components & Cleanup

Components are just functions that return elements. @jdlanglois/els handles memory management by tracking subscriptions and cleaning them up when an element is removed.

import { el, remove } from '@jdlanglois/els';

const UserProfile = (name: string) => {
  return el('div.profile', 
    el('h2', name),
    { onremove: () => console.log('Cleaning up...') }
  );
};

const profile = UserProfile('Alice');
// ... later ...
remove(profile); // Recursively stops all child streams and triggers onremove

Advanced Features

Keyed Lists

For large collections, use list() for efficient reconciliation. It surgically moves DOM nodes instead of recreating them.

import { el, list, stream } from '@jdlanglois/els';

const users = stream([{ id: 1, name: 'Alice' }]);

const view = el('ul', 
  list(users, u => u.id, (user, index) => 
    el('li', user.map(u => u.name))
  )
);

Routing

The built-in router supports both Hash and History API modes, with automatic link interception.

import { createRouter, tags } from '@jdlanglois/els';
const { h1, div } = tags;

const router = createRouter({ useHash: false });

const app = div(
  router.view({
    '/': () => h1('Home'),
    '/users/:id': (params) => h1(`User: ${params.id}`),
    '*': () => h1('404')
  })
);

Styling (CSS-in-JS)

Write scoped CSS using tagged template literals.

import { css } from '@jdlanglois/els';

const cardStyle = css`
  padding: 20px;
  &:hover { transform: scale(1.05); }
  & span { color: blue; }
`;

const globalStyles = css.global`
  body { margin: 0; background: #eee; }
`;

const myCard = el('div', { class: cardStyle }, 'Hover me!');

---

## Alternative Syntax Options

While the builder-pattern (`el`) is the fastest and smallest, you can also use these optional syntaxes for a more declarative feel.

### 1. HTML Tagged Templates (`@jdlanglois/els/html`)
For a `lit-html` like experience without needing a transpiler.

```typescript
import { html } from '@jdlanglois/els/html';
import { stream } from '@jdlanglois/els';

const count = stream(0);
const view = html`
  <div class="counter">
    <p>Count: ${count}</p>
    <button @click="${() => count(count() + 1)}">Increment</button>
  </div>
`;

2. JSX / TSX (@jdlanglois/els/jsx)

For the most familiar declarative experience. Requires a transpiler (Vite, Bun, etc.).

Configuration (tsconfig.json):

{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  }
}

Usage:

/** @jsx h */
import { h } from '@jdlanglois/els/jsx';
import { stream } from '@jdlanglois/els';

const App = () => {
  const count = stream(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onclick={() => count(count() + 1)}>Increment</button>
    </div>
  );
};

Production Optimization (Vite)

To achieve zero runtime overhead for styles, use the Vite plugin. It extracts css template literals into a static CSS file during build.

// vite.config.ts
import elPlugin from '@jdlanglois/els/vite';

export default {
  plugins: [elPlugin()]
};

Technical Appendix

Subscription Management

Subscriptions created via streams are stored using Symbol.for('el-subscriptions') on the DOM node. The remove(el) utility recursively traverses the tree to call .end(true) on every tracked subscription, ensuring no memory leaks.

Attribute vs Property

el smartly decides how to apply props:

  1. If the key exists as a property (e.g., className, onclick, value), it sets it directly.
  2. If it's a style or class object, it handles merging/toggling.
  3. Otherwise, it falls back to setAttribute.