In the browser DOM, what does Element.closest('.row') return, and how does it differ from Element.matches('.row')?
answer
- one looks up, one looks only here
- boolean versus element-or-null
- the starting element counts too
- proximity beats selector order
- the walk ends at its own tree's root
basics
~10 sElement.closest('.row') walks up from the element itself through its ancestors and returns the nearest element matching the selector, or null. Element.matches('.row') only tests that one element and returns a boolean.
solid answer
~50 s`closest()` searches **upwards**. It tests the element itself first, then its parent, grandparent and so on, and returns the first `Element` that matches the selector, or `null` if it reaches the top of the tree without a match. That inclusive-self step is the part people forget: if the element already has class `row`, `el.closest('.row')` returns `el`, not an ancestor. `matches()` searches nothing — it answers a yes/no question about the one element you called it on and returns a boolean. So `closest` is the tool for "which container am I in", and `matches` is the tool for "is this the thing I care about". Both take a full CSS selector list and both throw `SyntaxError` on a malformed one. `closest` walks only within its own tree: it stops at that tree's root and does not cross out of a shadow root.
code
javascript · 12 linesdocument.body.insertAdjacentHTML('beforeend',
'<div class="row" data-row-id="7"><span class="row"><button id="b">Go</button></span></div>');
const btn = document.getElementById('b');
console.log(btn.matches('button')); // true
console.log(btn.closest('.row').tagName); // "SPAN" — nearest wins
console.log(btn.closest('.card')); // null
const span = btn.parentElement;
console.log(span.closest('.row') === span); // true — inclusive of itself
console.log(span.parentElement.closest('.row').tagName); // "DIV" — start one level up
console.log(span.closest('[data-row-id]').dataset.rowId); // "7"go deeper
Know that closest() gives you the nearest matching container going up and null when there is none, while matches() is just a true/false test on one element.
Be ready to spell out the walk — element first, then each parent element — and to say that closest() returns null rather than throwing, but does throw SyntaxError on a malformed selector.
Show that you use closest() to keep code independent of exact nesting depth instead of chaining parentElement hops, and that you know it stops at the root of its own tree, including a shadow root.
Own the convention for what those selectors are allowed to name — a stable data attribute contract that survives markup and styling refactors, rather than class names owned by CSS.
## Two questions, two methods Both methods are on `Element` and both are backed by the same selector engine, but they answer different questions. `element.matches(selectors)` asks: *does this element match?* It returns `true` or `false` and touches nothing else in the tree. `element.closest(selectors)` asks: *what is the nearest matching element at or above this one?* It returns an `Element` or `null`. ```js const btn = document.querySelector('#save'); btn.matches('button[type="submit"]'); // true / false btn.closest('form'); // the <form> element, or null ``` ## The exact algorithm of closest() `closest` walks the **inclusive ancestor** chain. The steps are: 1. Start at the element itself. 2. If the current element matches the selector, return it. 3. Move to the parent element and repeat. 4. If there is no parent element left, return `null`. Two details follow directly from that. **It includes the element itself.** `el.closest('.row')` on an element that already carries `.row` returns `el`. This is the single most common misreading — people expect "closest ancestor" and are surprised when the starting node comes back. When you specifically need a strict ancestor, start one level up: `el.parentElement?.closest('.row')`. **It stops at the root of the current tree.** The chain is made of *elements*, so it ends at `<html>` in a document — `document` is a `Document`, not an `Element`, so `closest` never returns it. In a detached subtree the walk simply ends at the topmost detached element, which is why `closest` still works on nodes that are not in the document. Inside a shadow tree the walk ends at the shadow root, so `closest` does not escape into the host's tree. ```js document.documentElement.closest('html'); // <html> document.documentElement.closest('body'); // null — never goes downwards ``` `closest` never searches downwards or sideways. If the thing you want is inside, that is `querySelector`; if it is a sibling, you need to go up and then query down. ## Selector lists and nearest-wins Both methods accept a selector *list*. For `closest`, proximity wins over list order: `el.closest('.row, .card')` returns whichever of the two is encountered first walking up, regardless of which one you wrote first. That makes it a clean way to ask "which of these containers am I in", but it also means you cannot infer *which* selector matched from the return value alone — test the returned element with `matches` if you need to branch: ```js const container = el.closest('.row, .card'); if (container?.matches('.card')) { /* card behaviour */ } ``` That pairing — `closest` to find, `matches` to classify — is the idiomatic combination. ## Errors and null handling A malformed selector throws `SyntaxError` from both methods; they do not return `null`/`false` for bad syntax. Anything interpolated from data must be escaped with `CSS.escape()`. Because `closest` returns `null` for "no such container", the return value is a natural guard: ```js const row = el.closest('[data-row-id]'); if (!row) return; // not inside a row at all const id = row.getAttribute('data-row-id'); ``` Compare this with the manual loop it replaces: ```js let node = el; while (node && !node.matches('[data-row-id]')) node = node.parentElement; ``` The hand-written version is what `closest` does, and writing it out is a fine way to prove you understand the method — but shipping it is unnecessary, since `closest` has been supported everywhere for years. ## Choosing between them Reach for `matches` when you already hold the element and only need a predicate — filtering an array of elements, asserting a precondition, branching on a class or attribute state. Reach for `closest` when you hold a deep node and need the meaningful container above it: the row, the card, the form, the dialog, the element carrying the id you need. Both keep your code declarative in terms of selectors rather than hard-coded parent hops such as `el.parentElement.parentElement`, which break the moment someone adds a wrapper.
- How do you get the nearest strictly-ancestor match, excluding the element itself?Start the walk one level higher: `el.parentElement?.closest('.row')`. Because `closest` is inclusive of its starting element, there is no flag for this — you move the start point instead. The optional chaining matters, since `parentElement` is `null` for a detached root or for `<html>`.
- If you call el.closest('.row, .card'), which one do you get when both exist above the element?Whichever is nearer in the ancestor chain — the walk returns on the first match, so proximity decides, not the order you wrote the selectors in. If your code must branch on which one it found, test the result with `matches('.card')` afterwards rather than assuming list order carried through.
- Does closest() work on an element that is not attached to the document?Yes. It walks `parentElement` links, and a detached subtree still has them, so it returns the nearest match within that subtree and `null` once it runs out of parents. This is why it is safe to use on nodes you have built but not yet inserted, and why a `null` result does not by itself prove the element is detached — check `isConnected` for that.
saying these in an interview costs you the question
- Says closest only looks at ancestors, never the element itself
- Thinks matches() returns the matching element
- Expects closest() to be able to find a descendant
- Believes closest() returns the document when nothing matches
- Assumes the first selector in the list wins over a nearer match