Himanshu SharmaSoftware engineer
Let’s talk

jQuery · Lesson 26 of 45

Event delegation for elements added later

Use jQuery event delegation with .on(event, selector, handler) so one handler on a container works for every matching child, including elements added later.

12 minEventsUpdated 23 October 2026

By the end of this lessonAttach a single delegated handler that keeps working as items are added, removed or moved, and explain how it uses event bubbling.

The problem

In the events lesson you saw that .on() only attaches handlers to elements that exist when it runs. In real pages, elements come and go all the time: rows added from Ajax, to-do items created by the user, search results replaced on every search. Attaching handlers to each new element is tedious and easy to forget.

The solution: listen on the container

Events bubble: a click on a list item is also a click on the list, on the body, and on the document. So you can attach one handler to a container that always exists, and let it check where each click came from. jQuery does the checking for you when you pass a selector as the second argument:

html
<ul id="list"><li>First item</li></ul>
<button id="add">Add item</button>

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
  let n = 1;

  // Delegated: attached to #list, runs for any li inside, now or later
  $("#list").on("click", "li", function () {
    console.log("clicked:", $(this).text());
  });

  $("#add").on("click", function () {
    n++;
    $("#list").append("<li>Item " + n + "</li>");
  });
</script>
result
clicked: Item 3

Two items were added after the handler was attached, and clicking the newest one still worked. Compare this with the example in the events lesson, where the new item did nothing.

How it works

When a click reaches #list, jQuery looks at event.target and walks up from it to #list, checking each element against the selector "li". For every match, it calls your handler with this set to the matching element. So:

  • this (and event.currentTarget) is the matched li, not the list;
  • event.delegateTarget is the element the handler is attached to, #list;
  • event.target is still the innermost element clicked, which may be inside the li.
html
<table border="1" cellpadding="4">
  <tbody id="rows">
    <tr data-id="11"><td>Lamp</td><td><button class="del">Delete</button></td></tr>
    <tr data-id="12"><td>Desk</td><td><button class="del">Delete</button></td></tr>
  </tbody>
</table>

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
  $("#rows").on("click", ".del", function (event) {
    const $row = $(this).closest("tr");
    console.log("this:", this.className, "| delegateTarget:", event.delegateTarget.id, "| row id:", $row.data("id"));
    $row.remove();
    console.log("rows left:", $("#rows tr").length);
  });
</script>
result
this: del | delegateTarget: rows | row id: 12
rows left: 1

Why it is better

Delegation has three advantages:

  1. New elements just work. No need to attach handlers after every insert.
  2. One handler instead of many. A table with 1,000 rows needs one handler, not 1,000, which saves memory and setup time.
  3. Removing elements is clean. Removed rows had no handlers of their own, so nothing needs tidying.

That is why delegation is the normal way to handle events in lists, tables, menus and anything built from data.

A complete example

A small task list, using delegation for both actions on each item:

html
<style> .is-done span { text-decoration: line-through; color: gray; } li button { margin-left: 6px; } </style>
<form id="new"><input id="task" placeholder="New task"> <button>Add</button></form>
<ul id="tasks">
  <li><span>Pay rent</span><button class="done">Done</button><button class="remove">Remove</button></li>
</ul>

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
  $("#new").on("submit", function (event) {
    event.preventDefault();
    const text = $("#task").val().trim();
    if (!text) return;
    $("<li>").append(
      $("<span>", { text }),
      $("<button>", { "class": "done", text: "Done" }),
      $("<button>", { "class": "remove", text: "Remove" })
    ).appendTo("#tasks");
    $("#task").val("");
  });

  $("#tasks")
    .on("click", ".done", function () {
      $(this).closest("li").toggleClass("is-done");
    })
    .on("click", ".remove", function () {
      $(this).closest("li").remove();
    });

  $("#tasks").on("click", "button", function () {
    console.log($("#tasks li").map((i, el) => $(el).find("span").text() + ($(el).hasClass("is-done") ? " (done)" : "")).get().join(", "));
  });
</script>
result
Pay rent (done), Water plants
Pay rent (done)

The new task, added after the handlers were attached, could still be removed. The project at the end of the course builds a fuller version of this.

Choosing the container

Attach the delegated handler to the closest ancestor that always exists:

  • good: $("#tasks").on("click", ".remove", ...);
  • works, but less good: $(document).on("click", ".remove", ...).

Delegating to document is sometimes necessary (for example, when the container itself is replaced by Ajax), but then every click anywhere on the page has to be checked against your selector, and a stopPropagation() somewhere deeper can stop it from ever arriving.

Events that do not bubble

Delegation depends on bubbling. Most events bubble, but a few do not: focus, blur, mouseenter, mouseleave, load and scroll (on elements). jQuery quietly handles the common cases for you: a delegated focus or blur handler is converted to focusin/focusout, and delegated mouseenter/mouseleave are emulated with mouseover/mouseout. For load and scroll, attach directly.

Old methods you may meet

Before .on() existed, delegation was written as .live() (removed in jQuery 1.9) or $(container).delegate(selector, event, fn) (deprecated; note the different argument order). Rewrite both as $(container).on(event, selector, fn).

Common mistakes

  • Forgetting the selector argument, which makes a normal direct handler.
  • Delegating to a container that is itself replaced, which throws the handler away.
  • Using this expecting the container; it is the matched child.
  • Delegating everything to document by habit.

Exercise

Build a list of three colours. Using one delegated handler on the list, clicking any item should log its text. Add a button that adds a fourth colour, and check that clicking it also works.

Show one possible answer
html
<ul id="colours"><li>Red</li><li>Green</li><li>Blue</li></ul>
<button id="more">Add purple</button>

<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
  $("#colours").on("click", "li", function () { console.log($(this).text()); });
  $("#more").on("click", function () { $("#colours").append("<li>Purple</li>"); });
</script>
result
Purple

Key points

  • A delegated handler is attached to a container and runs for events that bubble up from children matching a selector.
  • Write it as $(container).on("click", ".item", handler); inside, this is the matching child.
  • It works for elements added after the handler was attached, and uses one handler instead of hundreds.
  • Attach to the closest container that always exists, not to document, and only for events that bubble.

Check yourself

1. In $("#list").on("click", "li", fn), what is this inside fn?
2. Why does a delegated handler work for elements added later?
3. Which is the best element to delegate a table's row clicks to?
4. What is the old jQuery method for delegation that was replaced by .on()?