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.1.2

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();    // → <my-widget>
  benchRow();    // → <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 } });
// → 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 at the top of a method rather than in the callback:

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

const { div, span } = tags; // Must be used inside a render() call

render(html(() => {
  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()

Run builder calls outside the active build context. Use this inside event handlers or async callbacks that fire while a render() pass is in progress — without detaching, those calls would incorrectly advance the parent cursor:

html((b) => {
  b.button({
    '@click': () => b.detached(() => {
      // Safe to call builder methods here even if the click fires mid-render.
      b.div({ text: 'appended' });
    }),
  });
});

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: (r: Reconciler) => void) => RenderResult | Creates a deferred render template | | render | (result: RenderResult, root: Node) => void | Applies a template to a root node | | tags | Reconciler (proxy) | Delegates to the active Reconciler | | getReconciler | () => Reconciler | Returns the active Reconciler (throws if none) | | Reconciler | class | Core cursor-based DOM reconciler | | TagProps | type | Prop bag accepted by all tag helpers | | RenderResult | class | Opaque wrapper returned by html() |

License

ISC