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).
on(el, 'click', fn)for classic bindingOn.click(el, fn)for fluent sugar per event nameOn.first.click(el, fn)fires once then unbindsOn.capture.passive.scroll(el, fn)for fully composable modifiersOn.delegate.click(el, selector, fn)for delegated eventsOn.hover(el, enter, leave)for mouseenter/leave pairsOn.batch(el, { click, ... })binds multiple events at onceOn.first.batch(...)for one-time multi-bindOn.ready(fn)runs when DOM is readyOn.group()collects related listeners and tears them down together- Better TS support for simple binds like
On.input(el, fn)andOn.change(el, fn) - ESM, zero dependencies, tiny footprint
pnpm add on-eventsAs 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.
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
thisbinding - Returns
stop()if you need manual control
Direct fluent binding per event name.
import { On } from 'on-events'
On.click(button, function handleClick() {
console.log('Clicked')
})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)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')
})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')
}]
})One-time version of batch().
On.first.batch(document, {
scroll: function handleFirstScroll() {
console.log('First scroll')
},
keyup: function handleFirstKeyup() {
console.log('First keyup')
}
})Runs fn once the DOM is fully loaded (DOMContentLoaded or already ready).
On.ready(function handleReady() {
console.log('DOM fully loaded')
})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()For custom or non-standard event names:
const stop = On.event('panel:open')(panel, (e) => {
console.log('opened', e.type)
})
stop()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() // unbindsSignatures:
on(el, event, handler)on(el, event, handler, options)on(el, event, selector, handler)for delegated eventson(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.
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
thisto 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
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 aclosest(selector)lookup per event. Ideal for reducing listener count, but direct binding is better for extremely hot events likemousemove.
Rule of thumb: delegate click, input, and submit; bind directly for high-frequency events.
- Delegation uses
Element.closest()internally. - In delegated handlers,
thisrefers to the matched element. - Modern browsers only (uses
Proxy,WeakMap, and modern DOM APIs). - All binding methods return a
stop()function for explicit cleanup.
On.once.* is available as a backward-compatible alias for On.first.*.
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.