Lesson 7 of 12

jQuery Traversing

Selectors find elements from the top of the page. But inside an event handler you usually start from one element – the button that was clicked – and need its neighbours: “the row this button is in”, “the price cell in that row”, “the other tabs next to this one”. Moving from one element to related ones is called traversing, and jQuery traversing is where chaining feels most natural.

Thinking about jQuery traversing as a family tree

The DOM is a tree, and jQuery traversing methods use family words. An element’s parent is the element directly around it; its children are the elements directly inside it. Ancestors are the parent, grandparent and so on up to <html>; descendants are everything inside at any depth. Siblings share the same parent.

DirectionMethodReturns
Up.parent()The direct parent of each element
Up.parents(sel)All ancestors (optionally filtered by a selector)
Up.closest(sel)The element itself or the nearest ancestor that matches – just one per element
Down.children(sel)Direct children only
Down.find(sel)Matching descendants at any depth
Sideways.siblings(sel)All other children of the same parent
Sideways.next() / .prev()The element directly after or before
Sideways.nextAll() / .prevAll()All following or preceding siblings

Most of these accept an optional selector to filter the result, for example .siblings('.active') or .next('p'). Note that .next('p') does not search forward for the next paragraph: it returns the very next sibling only if it is a paragraph, otherwise nothing.

closest() and find(): the most useful pair

The pattern you will use again and again is: go up to a container with .closest(), then down to what you need with .find(). It does not break when a designer wraps the button in an extra <div> or <span>, unlike a chain of .parent().parent() calls, which depends on the exact nesting.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Cart rows with closest() and find()</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    table { border-collapse: collapse; }
    th, td { border: 1px solid #ccc; padding: 6px 10px; }
    td.num { text-align: right; }
  </style>
</head>
<body>
  <table id="cart">
    <thead><tr><th>Item</th><th>Qty</th><th>Price (₹)</th><th></th></tr></thead>
    <tbody>
      <tr><td>Jasmine rice 5 kg</td><td class="num qty">1</td><td class="num price">450</td><td><button type="button" class="plus">+1</button> <button type="button" class="remove">Remove</button></td></tr>
      <tr><td>Toor dal 1 kg</td><td class="num qty">2</td><td class="num price">160</td><td><button type="button" class="plus">+1</button> <button type="button" class="remove">Remove</button></td></tr>
      <tr><td>Coffee powder</td><td class="num qty">1</td><td class="num price">240</td><td><button type="button" class="plus">+1</button> <button type="button" class="remove">Remove</button></td></tr>
    </tbody>
  </table>
  <p>Total: ₹<strong id="total">0</strong></p>

  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <script>
function updateTotal() {
  let total = 0;
  // .each() runs a function for every row; "row" is a plain DOM element
  $('#cart tbody tr').each((index, row) => {
    const $row = $(row);
    total += Number($row.find('.qty').text()) * Number($row.find('.price').text());
  });
  $('#total').text(total);
}

$('#cart').on('click', '.plus', (event) => {
  const $qty = $(event.currentTarget).closest('tr').find('.qty');
  $qty.text(Number($qty.text()) + 1);
  updateTotal();
});

$('#cart').on('click', '.remove', (event) => {
  $(event.currentTarget).closest('tr').remove();
  updateTotal();
});

updateTotal();
  </script>
</body>
</html>

.each() gives your callback the index and the plain DOM element, so wrap it with $(row) to use jQuery methods. Inside .each(), returning false stops the loop early, like break. For simply applying the same change to every element you do not need .each() at all – implicit iteration does it for you.

Siblings: one active item at a time

Accordions, tabs and star ratings all follow the rule “make this one active and the others not”. .siblings() expresses that directly. The demo below is an FAQ where opening one answer closes the others.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>An FAQ with closest() and siblings()</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    .faq { border: 1px solid #ddd; border-radius: 6px; margin-bottom: 6px; }
    .faq button { width: 100%; text-align: left; padding: 10px; border: 0; background: #f5f5f5; font: inherit; cursor: pointer; }
    .answer { display: none; padding: 0 10px 10px; }
    .faq.open .answer { display: block; }
    .faq.open button { background: #e8f0ff; }
  </style>
</head>
<body>
  <div id="faqs">
    <div class="faq open">
      <button type="button" aria-expanded="true">Do you deliver in Puducherry?</button>
      <p class="answer">Yes, within 10 km of White Town, every day except Monday.</p>
    </div>
    <div class="faq">
      <button type="button" aria-expanded="false">Can I pay by UPI?</button>
      <p class="answer">Yes, UPI and cards are accepted on delivery.</p>
    </div>
    <div class="faq">
      <button type="button" aria-expanded="false">Do you have sugar-free sweets?</button>
      <p class="answer">A small range – ask for the sugar-free box.</p>
    </div>
  </div>

  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <script>
$('#faqs').on('click', 'button', (event) => {
  const $faq = $(event.currentTarget).closest('.faq');
  const opening = !$faq.hasClass('open');

  // Close every other FAQ…
  $faq.siblings().removeClass('open').find('button').attr('aria-expanded', 'false');

  // …then open or close this one
  $faq.toggleClass('open', opening);
  $faq.find('button').attr('aria-expanded', String(opening));
});
  </script>
</body>
</html>

Tip: The aria-expanded attribute tells screen-reader users whether each answer is open. Keeping it in sync is one extra line, and it makes the component usable for everyone.

Filtering and testing a set

  • .filter(sel) keeps the matching elements; .not(sel) removes them.
  • .has(sel) keeps elements that contain a match, e.g. list items that have a link inside.
  • .is(sel) returns true or false, e.g. $box.is(':checked') or $el.is('.open').
  • .end() steps back to the previous set in a chain, and .addBack() adds the previous set to the current one. (The old .andSelf() was removed; use .addBack().)

Reading a long chain

Each traversal method returns a new jQuery object, and jQuery remembers the set you came from. That is what makes .end() possible. In $('#menu').find('a').addClass('link').end().addClass('ready'), the first addClass acts on the links; .end() steps back to #menu, so the second acts on the menu itself.

Chains like this are compact but can be hard to read a month later. If a chain goes past three or four steps, or you use the same set twice, store the intermediate result in a well-named variable such as $row or $links. The name explains what the set is, and the next person to edit the file does not have to replay the whole chain in their head.

// jQuery
$btn.closest('tr').find('.qty');
$item.siblings().removeClass('active');

// Plain JavaScript
btn.closest('tr').querySelector('.qty');
[...item.parentElement.children]
  .filter((el) => el !== item)
  .forEach((el) => el.classList.remove('active'));

Modern browsers have closest(), querySelector(), parentElement and nextElementSibling, so going up or down is just as short in plain JavaScript. Siblings and whole-set operations are where jQuery traversing still saves the most typing.

Further reading

Key takeaways

  • Traversal methods move up (parent, parents, closest), down (children, find) and sideways (siblings, next, prev).
  • Go up with .closest() and down with .find() – it survives changes in the HTML nesting.
  • .siblings() makes "one active item at a time" patterns easy.
  • .each() loops with (index, element); wrap the element with $() to use jQuery methods.
  • filter, not, has and is narrow or test a set; use addBack() instead of the removed andSelf().

Quick quiz

Check what you learned – pick an answer.

1. A Remove button sits somewhere inside a table row. Which line reliably gets that row?

2. What is the difference between .children() and .find()?

3. Which method replaced the removed .andSelf()?

Practice exercise

In the cart table, add a "−1" button to each row that lowers the quantity by one, removes the row when it reaches zero, and updates the total.

Show a hint

Use $(event.currentTarget).closest('tr') to get the row, and .find('.qty') to read and write the quantity.

Show the solution
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Exercise: traversing</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 16px; line-height: 1.5; }
    table { border-collapse: collapse; }
    th, td { border: 1px solid #ccc; padding: 6px 10px; }
    td.num { text-align: right; }
  </style>
</head>
<body>
  <table id="cart">
    <thead><tr><th>Item</th><th>Qty</th><th>Price (₹)</th><th></th></tr></thead>
    <tbody>
      <tr><td>Jasmine rice 5 kg</td><td class="num qty">1</td><td class="num price">450</td><td><button type="button" class="minus">−1</button> <button type="button" class="remove">Remove</button></td></tr>
      <tr><td>Toor dal 1 kg</td><td class="num qty">2</td><td class="num price">160</td><td><button type="button" class="minus">−1</button> <button type="button" class="remove">Remove</button></td></tr>
      <tr><td>Coffee powder</td><td class="num qty">1</td><td class="num price">240</td><td><button type="button" class="minus">−1</button> <button type="button" class="remove">Remove</button></td></tr>
    </tbody>
  </table>
  <p>Total: ₹<strong id="total">0</strong></p>

  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <script>
function updateTotal() {
  let total = 0;
  $('#cart tbody tr').each((i, row) => {
    total += Number($(row).find('.qty').text()) * Number($(row).find('.price').text());
  });
  $('#total').text(total);
}

$('#cart').on('click', '.minus', (event) => {
  const $row = $(event.currentTarget).closest('tr');
  const qty = Number($row.find('.qty').text()) - 1;

  if (qty <= 0) {
    $row.remove();
  } else {
    $row.find('.qty').text(qty);
  }
  updateTotal();
});

$('#cart').on('click', '.remove', (event) => {
  $(event.currentTarget).closest('tr').remove();
  updateTotal();
});

updateTotal();
  </script>
</body>
</html>

Progress is saved in this browser only.