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

drowjs

v1.0.4

Published

The Tiny, Object-Based Web Component Library.

Readme

Gitpod ready-to-code

Drow.js 🎭

The Tiny, Object-Based Web Component Library.

Drow is a minimalist wrapper for the Web Components API. It replaces the boilerplate of JavaScript Classes with a clean, object-based configuration. Define your components as simple objects and let Drow handle the registration and rendering.

Why Drow?

  • 🚫 Zero Dependencies: No NPM, no build steps, no headaches.
  • 📉 Microscopic Size: Tiny footprint with high performance.
  • 🧩 Object-First API: No more class X extends HTMLElement or super().
  • Native Performance: Uses the browser's built-in Custom Elements registry.
  • 🔁 Batched Rendering: Rapid state mutations are coalesced into a single DOM update per frame.

Setup

Include drow.js in your HTML file:

<script src="drow.js"></script>

Or as an NPM module:

npm install drowjs
import Drow from 'drowjs';

Define a Drow Component

Drow components are defined using simple objects. Reactivity is handled automatically via the state object and methods.

const config = {
  name: "my-counter",
  state: { count: 0 },
  css: `button { font-weight: bold; }`,
  template: `
    <div>
      <button @click="increment">Count is {{count}}</button>
    </div>
  `,
  methods: {
    increment() {
      this.state.count++;
    }
  }
};

Drow.register(config);

Config Reference

| Key | Type | Description | | :--- | :--- | :--- | | name | string | Required. Custom element tag name — must contain a hyphen. | | template | string | HTML template string. Supports {{expression}} interpolation. | | state | object | Reactive per-instance state. Changes trigger a batched re-render. | | methods | object | Event handler functions. Called with this bound to the component. | | computed | object | Derived values: { key: (state) => value }. Available in templates. | | props | string[] | Observed HTML attributes, exposed as template variables. | | css | string | Scoped CSS. Use :host to target the component root. | | shadow | boolean | Attach a Shadow DOM instead of rendering in the Light DOM. | | useStore | boolean | Subscribe to the global store. Access values via {{store.key}}. | | init | function | Lifecycle hook — called once after the component connects. | | disconnected | function | Lifecycle hook — called when the component is removed from the DOM. | | updated | function | Lifecycle hook — called after every render cycle. | | watch | function(attr) | Called when an observed prop attribute changes. Receives { name, oldValue, newValue, comp }. |


Directives Reference

| Directive | Description | Example | | :--- | :--- | :--- | | @event | Bind DOM events. Supports .prevent and .stop modifiers. | @click="doSomething" | | @event.prevent | Calls preventDefault() before the handler. | @submit.prevent="save" | | @event.stop | Calls stopPropagation() before the handler. | @click.stop="select" | | d-model | Two-way binding for inputs. Supports nested paths. | d-model="user.name" | | d-for | Render a list. Supports objects and (item, index) syntax. | d-for="item in items" | | d-key | Keyed diffing for d-for — patches only changed items. | d-key="{{item.id}}" | | d-if | Conditional rendering — removes element from DOM. | d-if="isVisible" | | d-else-if | Chained condition after d-if. | d-else-if="otherFlag" | | d-else | Fallback block after d-if or d-else-if. | d-else | | d-show | Toggles display: none — element stays in DOM. | d-show="isVisible" | | d-class:name | Conditionally adds/removes a CSS class. | d-class:active="isActive" | | d-bind:attr | Reactive attribute binding. | d-bind:src="imageUrl" | | d-html | Sets innerHTML from a state value. Use with trusted content only. | d-html="richContent" | | d-ref | Registers element in this.refs for direct DOM access. | d-ref="myInput" | | d-cloak | Hidden until the component finishes rendering (prevents FOUC). | d-cloak |


Interpolation

Use {{key}} in templates to render state, computed, prop, or store values. Dot notation is supported for nested access.

<p>{{user.profile.name}}</p>
<p>{{store.currentUser}}</p>

Event Binding

Bind any DOM event with @eventName="methodName". Modifiers can be chained with .:

<button @click="save">Save</button>
<form @submit.prevent="handleSubmit">...</form>
<div @click.stop="select">...</div>

Two-Way Binding (d-model)

d-model syncs an input's value with state. It supports nested paths:

<input d-model="username" />
<input d-model="address.city" />
<input type="checkbox" d-model="settings.darkMode" />

List Rendering (d-for)

Iterate over arrays of primitives or objects:

<!-- Primitive list -->
<li d-for="tag in tags">{{tag}}</li>

<!-- Object list -->
<li d-for="user in users">{{user.name}} — {{user.email}}</li>

<!-- With index -->
<li d-for="(item, index) in items">{{index}}. {{item}}</li>

Keyed Diffing (d-key)

Add d-key to enable efficient patching — Drow will reuse existing DOM nodes instead of rebuilding the entire list on every render:

<li d-for="item in items" d-key="{{item.id}}">{{item.name}}</li>

Use a value that uniquely identifies each item (e.g. an id). Without d-key, the list is rebuilt from scratch on each render.


Conditional Rendering (d-if / d-else-if / d-else)

<p d-if="status === 'loading'">Loading…</p>
<p d-else-if="error">Something went wrong.</p>
<p d-else>Ready.</p>

d-else-if and d-else must immediately follow a d-if (or another d-else-if) element with no elements between them.

Use d-show instead of d-if when the element should remain in the DOM and only its visibility should toggle:

<div d-show="isPanelOpen">...</div>

Computed Properties

Computed values derive from state and are re-evaluated on every render:

Drow.register({
  name: "cart-summary",
  state: { items: [] },
  computed: {
    total: (state) => state.items.reduce((sum, item) => sum + item.price, 0),
    isEmpty: (state) => state.items.length === 0
  },
  template: `
    <div>
      <p d-if="isEmpty">Your cart is empty.</p>
      <p d-else>Total: ${{total}}</p>
    </div>
  `
});

Lifecycle Hooks

Drow.register({
  name: "live-clock",
  state: { time: "" },
  init() {
    // Called once after the component connects to the DOM
    this._timer = setInterval(() => {
      this.state.time = new Date().toLocaleTimeString();
    }, 1000);
  },
  disconnected() {
    // Called when the component is removed from the DOM
    clearInterval(this._timer);
  },
  updated() {
    // Called after every render cycle
    console.log("clock re-rendered:", this.state.time);
  },
  template: `<span>{{time}}</span>`
});

Global Store

Share state across multiple components with Drow.store. Any component with useStore: true re-renders when the store changes.

// Write
Drow.store.state.user = "Alice";

// Read in template
// {{store.user}}
Drow.register({
  name: "user-badge",
  useStore: true,
  template: `<span>Logged in as {{store.user}}</span>`
});

Drow.register({
  name: "login-btn",
  useStore: true,
  methods: {
    login() {
      Drow.store.state.user = "Alice";
    }
  },
  template: `<button @click="login">Log In</button>`
});

Custom Events (this.emit)

Components can dispatch custom events that bubble up the DOM:

methods: {
  submit() {
    this.emit("form-submit", { data: this.state.formData });
  }
}

Listen from a parent:

document.querySelector("my-form").addEventListener("form-submit", (e) => {
  console.log(e.detail.data);
});

Refs (d-ref)

Access DOM elements directly via this.refs:

<input d-ref="emailInput" type="email" />
<button @click="focusEmail">Focus</button>
methods: {
  focusEmail() {
    this.refs.emailInput.focus();
  }
}

Scoped CSS

CSS defined in css is automatically scoped to the component. Use :host to target the component element itself:

css: `
  :host { display: block; padding: 1rem; }
  button { background: #a78bfa; color: white; }
`

For Shadow DOM components (shadow: true), standard :host CSS works natively.


Props & Attribute Watching

Declare observed attributes with props. They're available as template variables and trigger a re-render when changed. Use watch to respond to changes:

Drow.register({
  name: "user-card",
  props: ["username", "avatar"],
  watch({ name, oldValue, newValue }) {
    console.log(`${name} changed: ${oldValue} → ${newValue}`);
  },
  template: `
    <div>
      <img d-bind:src="avatar" />
      <p>{{username}}</p>
    </div>
  `
});
<user-card username="Alice" avatar="alice.png"></user-card>

Debug Mode

Drow.debug controls console output. It defaults to true. Set it to false before registering components in production to suppress all Drow logs and dev warnings:

Drow.debug = false;
Drow.register({ ... });

In debug mode, Drow will warn you if:

  • A component name is missing or doesn't contain a hyphen (required by the Custom Elements spec)
  • An unknown key is present in a config object

Interactive Demo: Todo List

This example demonstrates list rendering, two-way data binding, and event handling.

Drow.register({
  name: "todo-app",
  state: {
    newTask: "",
    tasks: ["Master Drow.js", "Build a tiny app"]
  },
  template: `
    <div class="todo-box">
      <h3>Task List ({{count}})</h3>
      <input d-model="newTask" placeholder="Add a new task...">
      <button @click="addTask">Add</button>
      <button @click="clearTasks">Clear All</button>
      <ul>
        <li d-for="(task, index) in tasks" d-key="{{task}}">
          <span>{{index}}. {{task}}</span>
          <button @click="removeTask" data-item="{{task}}">x</button>
        </li>
      </ul>
    </div>
  `,
  computed: {
    count: (state) => state.tasks.length
  },
  methods: {
    addTask() {
      if (this.state.newTask.trim()) {
        this.state.tasks = [...this.state.tasks, this.state.newTask];
        this.state.newTask = "";
      }
    },
    removeTask(e) {
      const item = e.target.dataset.item;
      this.state.tasks = this.state.tasks.filter(t => t !== item);
    },
    clearTasks() {
      this.state.tasks = [];
    }
  },
  css: `
    .todo-box { border: 1px solid #444; padding: 1rem; border-radius: 8px; }
    input { padding: 5px; border-radius: 4px; border: 1px solid #ccc; }
    button { cursor: pointer; background: #a78bfa; color: white; border: none; padding: 5px 10px; border-radius: 4px; }
    li { display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; }
    li button { background: #ef4444; padding: 2px 8px; }
  `
});

Examples

Check out the live interactive demos:


Documentation

Detailed API documentation is generated using JSDoc. To generate it locally:

npm run doc

The documentation is generated in the docs/ directory.


Local Development

npm install
npm run server

Building

npx terser drow.js -o drow.min.js --compress --mangle

Credits

Author johnfacey.dev