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

aberdeen

v1.9.0

Published

Build fast reactive UIs in pure TypeScript/JavaScript without a virtual DOM.

Readme

Aberdeen

Reactive UIs in plain TypeScript. Simple to learn, fast to ship.

Aberdeen wraps your state in ES6 Proxy objects for fine-grained property access tracking, then automatically re-executes only the DOM-building closures that depend on changed data. So we get precise DOM updates with neither virtual DOM diffing nor compiler magic.

Why use Aberdeen?

  • Simple: Express UIs naturally in JavaScript/TypeScript, without requiring build steps or JSX, and with a minimal amount of concepts you need to learn.
  • Type-safe: Your reactive state can be regular TypeScript objects and arrays, with full type safety and autocompletion.
  • Fast: No virtual DOM. Aberdeen intelligently updates only the minimal, necessary parts of your UI when proxied data changes.
  • Awesome lists: It's very easy and performant to reactively display data sorted by whatever you like.
  • Tiny: Around 7KB (minimized and gzipped) for the core system. Zero runtime dependencies.
  • Batteries included: Comes with...
    • Browser history management
    • Routing
    • Revertible patches for optimistic user-interface updates
    • Component-local CSS with Tailwind-like shorthands
    • SVG support
    • Helper functions for transforming reactive data (mapping, partitioning, filtering, etc)
    • Hide/unhide transition effects

Why not use Aberdeen?

  • Lack of community: There are not many of us -Aberdeen developers- yet, so don't expect terribly helpful Stack Overflow/AI answers.
  • Lack of ecosystem: You'd have to code things yourself, instead of duct-taping together a gazillion React ecosystem libraries.

Examples

First, let's start with the obligatory reactive counter example. If you're reading this on the official website you should see a working demo below the code, and an 'edit' button in the top-right corner of the code, to play around.

import {$, proxy, ref} from 'aberdeen';

// Define some state as a proxied (observable) object
const state = proxy({question: "How many roads must a man walk down?", answer: 42});

$('h3', () => {
    // This function reruns whenever the question or the answer changes
    $('text=', `${state.question} ↪ ${state.answer || 'Blowing in the wind'}`)
});

// Two-way bind state.question to an <input>
$('input placeholder=Question bind=', ref(state, 'question'))

// Allow state.answer to be modified using both an <input> and buttons
$('div.row margin-top:1em', () => {
    $('button text=- click=', () => state.answer--);
    $('input type=number bind=', ref(state, 'answer'))
    $('button text=+ click=', () => state.answer++);
});

Okay, next up is a somewhat more complex app - a todo-list with the following behavior:

  • New items open in an 'editing state'.
  • Items that are in 'editing state' show a text input, a save button and a cancel button. Done status cannot be toggled while editing.
  • Pressing one of the buttons, or pressing enter will transition from 'editing state' to 'viewing state', saving the new label text unless cancel was pressed.
  • In 'viewing state', the label is shown as non-editable. There's an 'Edit' link, that will transition the item to 'editing state'. Clicking anywhere else will toggle the done status.
  • The list of items is sorted alphabetically by label. Items move when 'save' changes their label.
  • Items that are created, moved or deleted grow and shrink as appropriate.

Pfew.. now let's look at the code:

import {$, proxy, onEach, insertCss, peek, unproxy, ref} from "aberdeen";
import {grow, shrink} from "aberdeen/transitions";

// We'll use a simple class to store our data.
class TodoItem {
    constructor(public label: string = '', public done: boolean = false) {}
    toggle() { this.done = !this.done; }
}

// The top-level user interface.
function drawMain() {
    // Add some initial items. We'll wrap a proxy() around it!
    let items: TodoItem[] = proxy([
        new TodoItem('Make todo-list demo', true),
        new TodoItem('Learn Aberdeen', false),
    ]);
    
    // Draw the list, ordered by label.
    onEach(items, drawItem, item => item.label);

    // Add item and delete checked buttons.
    $('div.row', () => {
        $('button text=+ click=', () => items.push(new TodoItem("")));
        $('button.outline text="Delete checked" click=', () => {
            for(let idx in items) {
                if (items[idx].done) delete items[idx];
            }
        });
    });
};

// Called for each todo list item.
function drawItem(item) {
    // Items without a label open in editing state.
    // Note that we're creating this proxy outside the `div.row` scope
    // create below, so that it will persist when that state reruns.
    let editing: {value: boolean} = proxy(item.label == '');

    $('div.row', todoItemStyle, 'create=', grow, 'destroy=', shrink, () => {
        // Conditionally add a class to `div.row`, based on item.done
        $({".done": ref(item,'done')});

        // The checkmark is hidden using CSS
        $('div.checkmark text=✅');

        if (editing.value) {
            // Proxied string to hold label while being edited.
            const labelCopy = proxy(item.label);
            function save() {
                editing.value = false;
                item.label = labelCopy.value;
            }
            // Label <input>. Save using enter or button.
            $('input placeholder=Label bind=', labelCopy, 'keydown=', e => e.key==='Enter' && save());
            $('button.outline text=Cancel click=', () => editing.value = false);
            $('button text=Save click=', save);
        } else {
            // Label as text. 
            $('p text=', item.label);

            // Edit icon, if not done.
            if (!item.done) {
                $('a text=Edit click=', e => {
                    editing.value = true;
                    e.stopPropagation(); // We don't want to toggle as well.
                });
            }
            
            // Clicking a row toggles done.
            $('cursor:pointer click=', () => item.done = !item.done);
        }
    });
}

// Insert some component-local CSS, specific for this demo.
const todoItemStyle = insertCss({
    "&": "mb:0.5rem",
    ".checkmark": "opacity:0.2",
    "&.done": "text-decoration:line-through",
    "&.done .checkmark": "opacity:1"
});

// Go!
drawMain();

Some further examples:

Learning Aberdeen

And you may want to study the examples above, of course!

AI Integration

If you use Claude Code, GitHub Copilot or another AI agents that supports Skills, Aberdeen includes a skill/ directory that provides specialized knowledge to the AI about how to use the library effectively.

To use this, it is recommended to symlink the skill into your project's .claude/skills directory:

mkdir -p .claude/skills
ln -s ../../node_modules/aberdeen/skill .claude/skills/aberdeen

Changelog

See CHANGELOG.md for a full history of changes.