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

aeico-view

v0.2.0

Published

DOM rendering layer for Aeico - html(), render(), Reconciler

Readme

aeico-view

Cursor-based DOM reconciler for the browser - framework-free, zero virtual DOM.
Works in plain HTML pages, vanilla web components, and as the rendering layer of the Aeico framework.
Provides html(), render(), tags, getReconciler(), and the Reconciler class.

How it works

aeico-view uses a cursor-based reconciler rather than a virtual DOM. On each render pass the Reconciler walks the existing child list in lock-step with the render callback. Nodes at the right position with the right tag are reused; only props that changed are written to the DOM. Nodes no longer visited are removed. No diffing tree is allocated - reconciliation happens directly against the live DOM.

Installation

npm install aeico-view

Usage

Plain HTML page

Drop it into any project - no framework, no build tool required beyond a bundler or native ES modules:

import { html, render } from 'aeico-view';

// html() accepts a callback - it is NOT a template literal tag.
const tpl = html(({ div, span, button }) => {
  div({ className: 'card' }, () => {
    span({ text: 'Hello world' });
    button({ text: 'Click me', '@click': () => alert('hi') });
  });
});

// render() applies the template to a root node.
// Repeated calls reuse the same Reconciler instance and only patch what changed.
render(tpl, document.querySelector('#app')!);

Vanilla web component (no base class required)

aeico-view works with any HTMLElement subclass. Use html() + render() directly inside your component methods - there is no required lifecycle adapter or mixin:

import { html, render } from 'aeico-view';

class MyCounter extends HTMLElement {
  #count = 0;
  shadow = this.attachShadow({ mode: 'open' });

  connectedCallback() { this.#render(); }

  increment() { this.#count++; this.#render(); }

  #render() {
    render(
      html(({ div, span, button }) => {
        div(() => {
          span({ text: String(this.#count) });
          button({ text: '+', '@click': () => this.increment() });
        });
      }),
      this.shadow,
    );
  }
}

customElements.define('my-counter', MyCounter);

Each call to #render() reconciles the shadow DOM in-place: only changed text or attributes are written, and the existing element nodes are reused.

Low-level: Reconciler directly

Skip html() / render() and drive the reconciler yourself:

import { Reconciler } from 'aeico-view';

const r = new Reconciler();
const root = document.querySelector('#app')!;

function draw(count: number) {
  r.build(root, () => {
    r.p({ text: `Count: ${count}` });
  });
}

draw(0); // initial render
draw(1); // patches only the text content

Template anatomy

Tag helpers

Every HTML and SVG tag is available as a method on the Reconciler.
The overloads are (props?, cb?) and (cb):

html(({ div, p, ul, li, svg, circle }) => {
  div({ id: 'wrapper' }, () => {
    p({ text: 'paragraph' });
    ul(() => {
      li({ text: 'item 1' });
      li({ text: 'item 2' });
    });
    // SVG namespace is auto-detected from the svg tag and inherited by children.
    svg({ viewBox: '0 0 100 100' }, () => {
      circle({ cx: '50', cy: '50', r: '40' });
    });
  });
});

Custom elements

camelCase property names are automatically converted to kebab-case tag names:

html(({ myWidget, benchRow }) => {
  myWidget();    // yields <my-widget>
  benchRow();    // yields <bench-row>
});

Dynamic tag names

Use el() when the tag is determined at runtime:

html((b) => {
  const tag = isHeading ? 'h1' : 'h2';
  b.el(tag, { text: title });
});

Text nodes

html((b) => {
  b.text('plain text node');
});

Fragments

fragment() creates a DocumentFragment context. Elements inside are always freshly appended (not reconciled), making it suitable for one-time subtree construction:

html((b) => {
  const frag = b.fragment(() => {
    b.li({ text: 'a' });
    b.li({ text: 'b' });
  });
  myList.appendChild(frag);
});

Pre-built nodes

Insert an existing node or fragment into the reconciled tree:

html((b) => {
  b.div(() => {
    b.node(someExternalNode);
  });
});

Props reference

| Prop | Type | Effect | |---|---|---| | text / textContent | string | Sets element.textContent | | className / class | string or Record<string, boolean> | Sets the class attribute | | style | Record<string, string> | Merges into inline style; supports --custom-props | | key | string | Keyed reconciliation hint - not written to the DOM | | @click, @input, … | EventListener | Adds event listener; old listener removed on change | | disabled, hidden, … | true | Presence-only attribute (setAttribute(k, '')) | | any other key | object | Assigned as a JS property | | any other key | string \| number | Set via setAttribute | | null \| false | - | Removes the attribute / listener |

Class object map

div({ class: { active: isActive, hidden: !isVisible } });
// yields class="active" when isActive=true, isVisible=true

Style object

span({ style: { color: 'red', '--my-var': '42px' } });

Event handlers

button({ '@click': handleClick, '@pointerenter': handleHover });

Keyed lists

Add a key prop to preserve element identity across list reorders.
The reconciler will move the existing DOM node to the correct position rather than recreating it:

html(({ ul, li }) => {
  ul(() => {
    for (const item of items) {
      li({ key: item.id, text: item.label });
    }
  });
});

tags - destructure outside the callback

tags is a proxy that always delegates to the currently active Reconciler.
Useful when you want to destructure helpers rather than taking them from the callback parameter.
Destructure inside a render() execution - resolving tags outside one throws:

import { html, render, tags } from 'aeico-view';

render(html(() => {
  const { div, span } = tags; // resolve inside the render context
  div(() => {
    span({ text: 'hello' });
  });
}), root);

getReconciler()

Returns the Reconciler that is currently executing inside a render() call.
Throws outside a render context. Useful in helper functions that need builder access without receiving it as a parameter:

import { getReconciler } from 'aeico-view';

function badge(label: string) {
  const b = getReconciler();
  b.span({ className: 'badge', text: label });
}

render(html(() => {
  badge('new');
}), root);

detached()

detached() is an engine method on Reconciler (not part of Tags), so access it via getReconciler():

import { getReconciler } from 'aeico-view';

html((b) => {
  b.div(() => {
    const tip = getReconciler().detached(() =>
      b.span({ className: 'tooltip', text: 'built off-cursor' }),
    );
    // `tip` was created fresh - NOT reconciled into b.div's children and
    // NOT appended anywhere.  Attach it manually wherever needed.
    document.body.appendChild(tip);
  });
});

What it does: temporarily clears the build context (parent stack + cursor), runs the callback in "fresh append" mode, then restores the previous context - even if the callback throws.

When you need it:

  • Shared helper code that may run during a render pass. Normal tag calls inside a build pass advance the cursor and get reconciled against existing children; code that must not disturb the in-progress traversal wraps its calls in detached().
  • Building a subtree to attach manually. Inside detached(), created elements have no parent - the callback's return value hands them to you for manual mounting (as shown above).

When you do not need it: event handlers like '@click'. A render pass is synchronous, so by the time a click fires the build context is already empty - builder calls there are already in fresh-append mode.

ShadowRoot support

render() accepts any Node as its root, including a ShadowRoot:

const shadow = host.attachShadow({ mode: 'open' });
render(html(({ div }) => { div({ text: 'shadow content' }); }), shadow);

API summary

| Export | Signature | Description | |---|---|---| | html | (cb: (t: Tags) => void) => Renderable | Creates a deferred render template | | render | (renderable: Renderable, root: Node) => void | Applies a template to a root node | | tags | Tags (proxy) | Tag view of the active Reconciler | | getReconciler | () => Reconciler | Returns the active Reconciler (throws if none) | | Reconciler | class | Core cursor-based DOM reconciler (Tags superset) | | Tags | type | Tag vocabulary passed to html() callbacks | | TagProps | type | Prop bag accepted by all tag helpers | | Renderable | class | Opaque wrapper returned by html() |