Engineering Journal
Table Formatter
Table Formatter

jQuery .off('keydown') on document removes handlers you didn't write

2026-07-16

TLDR

An Enter-key handler bound with $(document).on('keydown', '#input', fn) worked in tests and did nothing in the app. Another module ran $(document).off('keydown') to reset its own global shortcut, which detaches all keydown handlers on document, mine included. The fix: bind to the element itself, out of the blast radius.

Symptom

Pressing Return in a class-name field was supposed to add the class as a pill. In the jsdom test harness, triggering the event worked:
ok   class added to cell
ok   pill rendered for added class

In the running app: nothing. No error, no console output, no handler invocation. The classic signature of a handler that was bound and then silently removed, as opposed to one that throws.

Why it happens

jQuery's .off(events) with a bare event name removes every handler for that event on the selected element. Not your handlers. Every handler, from every module, including delegated ones, regardless of namespace.

The app's core module resets its global keyboard shortcuts like this:

$(document).off('keydown').on('keydown', function (e) {
    // global shortcut dispatch
});

Idempotent rebinding is a sensible goal: run it twice and you get one handler, not two. But document is shared infrastructure. My module, loaded later, delegated from the same element:

$(document).on('keydown', '#classInput', addPillOnEnter);

The next time the core module's reset line ran, my delegation was gone. Namespacing my handler would not have helped; a bare .off('keydown') removes namespaced handlers too. The namespace only protects you when the remover also scopes itself, as in .off('keydown.myModule').

The fix

// wrong: delegated from document, inside the reset's blast radius
$(document).on('keydown', '#classInput', addPillOnEnter);

// correct: bound to the element; document-level .off can't reach it $('#classInput').off('keydown.pills').on('keydown.pills', addPillOnEnter);

Handlers bound to #classInput live on that element, so a .off() against document never touches them. The namespace on my own binding keeps the rebind idempotent without endangering anyone else.

Evidence

Reproducible in three lines against any page with jQuery:
$(document).on('keydown', '#x', () => console.log('fires'));
$(document).off('keydown');     // someone else's "reset"
// pressing a key in #x now logs nothing

The jsdom tests passed because the harness never executed the other module's reset path; it loaded the panel module in isolation. The bug only existed in the composed app.

How to prevent it

Two rules, either one of which prevents the class of bug. Never call .off(event) un-namespaced on a shared element like document, window, or body; always scope the removal to your own namespace. And prefer binding to the most specific element that exists at bind time; delegation from document is for elements that do not exist yet, which a static input is not.

A grep for \.off\('(keydown|click|mouseup)'\) on shared elements is a cheap audit for an existing codebase.

The generalizable lesson

Event handlers on document are a shared namespace with no access control. Any module that bulk-removes there is deleting other people's code, and any module that binds there is trusting nobody will.
Read this post in the full Engineering Journal →