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.
| Direction | Method | Returns |
|---|---|---|
| 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)returnstrueorfalse, 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.
