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

on-events

v0.0.5

Published

Tiny DOM event utility with composable sugar, delegation, and cleanup.

Readme

on-events

npm downloads bundle size license stars

A tiny DOM event utility with composable sugar. Write clean event bindings using fluent chains like On.click(...), On.capture.passive.scroll(...), On.first.delegate.click(...), or classic on(el, 'click', fn).

Features

  • on(el, 'click', fn) for classic binding
  • On.click(el, fn) for fluent sugar per event name
  • On.first.click(el, fn) fires once then unbinds
  • On.capture.passive.scroll(el, fn) for fully composable modifiers
  • On.delegate.click(el, selector, fn) for delegated events
  • On.hover(el, enter, leave) for mouseenter/leave pairs
  • On.batch(el, { click, ... }) binds multiple events at once
  • On.first.batch(...) for one-time multi-bind
  • On.ready(fn) runs when DOM is ready
  • On.group() collects related listeners and tears them down together
  • Better TS support for simple binds like On.input(el, fn) and On.change(el, fn)
  • ESM, zero dependencies, tiny footprint

Install

pnpm add on-events

As of 0.0.5 the default entry routes bundlers at ESM source so unused exports can be tree-shaken; pre-built minified bundle remains available via the on-events/min subpath for unbundled <script type="module"> use.


Quick start

Compose once, capture, passive, and delegation without repetitive option objects:

import { On } from 'on-events'

function handleLinkClick(e) {
  e.preventDefault()
  console.log('First captured delegated click:', this.href)
}

On.first.delegate.capture.click(document, 'a.nav-link', handleLinkClick)
  • Delegated
  • Capture phase
  • Fires once
  • Clean this binding
  • Returns stop() if you need manual control

API

On.<event>(el, fn)

Direct fluent binding per event name.

import { On } from 'on-events'

On.click(button, function handleClick() {
  console.log('Clicked')
})

Composable modifiers

Modifiers can be chained before the event name. Order-independent.

Available modifiers:

  • first / once{ once: true }
  • capture{ capture: true }
  • passive{ passive: true }
  • delegate → enables delegated signature (el, selector, handler)
On.first.click(el, fn)
On.capture.scroll(window, fn)
On.passive.wheel(el, fn)
On.delegate.click(root, 'a', fn)
On.first.capture.passive.touchstart(el, fn)

On.hover(el, enterFn, leaveFn)

Convenience wrapper for mouseenter and mouseleave. Returns a single stop() function.

const stopHover = On.hover(card, function enterCard() {
  card.classList.add('hover')
}, function leaveCard() {
  card.classList.remove('hover')
})

On.batch(el, map)

Bind multiple events at once.

const stop = On.batch(window, {
  click: function handleWindowClick() {
    console.log('Window clicked')
  },
  keydown: function handleKeydown(e) {
    console.log('Key:', e.key)
  }
})

// Unbind all
stop()

Delegated entries are supported by passing [selector, handler]:

On.batch(document, {
  click: ['button.save', function handleSave() {
    console.log('Saved')
  }]
})

On.first.batch(el, map)

One-time version of batch().

On.first.batch(document, {
  scroll: function handleFirstScroll() {
    console.log('First scroll')
  },
  keyup: function handleFirstKeyup() {
    console.log('First keyup')
  }
})

On.ready(fn)

Runs fn once the DOM is fully loaded (DOMContentLoaded or already ready).

On.ready(function handleReady() {
  console.log('DOM fully loaded')
})

On.group()

Creates a scoped cleanup collector for related listeners. Useful when a page, modal, or UI module binds listeners across multiple elements and wants one teardown call.

import { On } from 'on-events'

const page = On.group()

page.click(settingsToggleBtn, () => {
  settingsSection.classList.toggle('collapsed')
})

page.input(searchInput, handleSearch)
page.delegate.click(document, 'button.save', handleSave)

// Later:
page.stop()

You can also manually add an existing cleanup function:

const group = On.group()

group.add(On.click(button, handleClick))
group.add(null) // safely ignored

group.stop()

On.event(type)

For custom or non-standard event names:

const stop = On.event('panel:open')(panel, (e) => {
  console.log('opened', e.type)
})

stop()

Low-level on / off

If you prefer a minimal, explicit API without fluent modifiers, use the core helpers directly.

import { on, off } from 'on-events'

function handleKeydown(e) {
  console.log('Pressed:', e.key)
}

const stop = on(window, 'keydown', handleKeydown)

stop() // unbinds

Signatures:

  • on(el, event, handler)
  • on(el, event, handler, options)
  • on(el, event, selector, handler) for delegated events
  • on(el, event, selector, handler, options)
  • off(el, event, handler, [selector]) removes a previously added listener

Adds a standard or delegated event listener. Returns a stop() function that removes the listener.


Notes

Why not addEventListener directly?

addEventListener is great, this library just removes the repetitive parts when you bind lots of UI events.

  • One-liners for common patterns (once, capture, passive, delegate)
  • Every bind returns a stop() cleanup function
  • Delegation helper that sets this to the matched element
  • Batch binding to keep setup code tidy
  • Grouped cleanup for lifecycle-based teardown
  • Composable modifiers instead of option object juggling
  • Zero deps and tiny footprint

Performance note

This library is a thin wrapper around native addEventListener.

  • Direct binding (On.click(el, fn) / on(el, 'click', fn)): essentially zero runtime overhead beyond one extra function call during setup.
  • first / capture / passive: uses native listener options.
  • Delegation (On.delegate.*): performs a closest(selector) lookup per event. Ideal for reducing listener count, but direct binding is better for extremely hot events like mousemove.

Rule of thumb: delegate click, input, and submit; bind directly for high-frequency events.

Misc

  • Delegation uses Element.closest() internally.
  • In delegated handlers, this refers to the matched element.
  • Modern browsers only (uses Proxy, WeakMap, and modern DOM APIs).
  • All binding methods return a stop() function for explicit cleanup.

Legacy alias

On.once.* is available as a backward-compatible alias for On.first.*.


License

Licensed under AGPL-3.0 with WATT3D Additional Terms. See LICENSE and ADDITIONAL_TERMS.md. Commercial AI/model-training use requires compliance with those terms or a separate WATT3D license. © WATT3D.