Tagged: web-api

  • Selector scoping in element.querySelector() and element.querySelectorAll()

    I was reading the MDN docs for element.querySelector() and learnt something totally unexpected about selector scope when this method is called on an element.

    Let’s say I have the following HTML:

    <div>
    	<p>This is a paragraph and it has a <span>span inside</span> it.</p>
    </div>
    
    <script>
    	const baseElement = document.querySelector('p');
    	// Note: We are querying relative to the <p> element, not the document
    	const matchedSpan = baseElement.querySelector('div span');
    	console.log(matchedSpan);
    </script>

    Once the above JavaScript is executed, what do you expect to happen?

    I expected it to log null to the console. My reasoning was simple: I am calling the method on baseElement (the <p> tag), and that paragraph clearly does not contain a <div> inside it.

    But here’s what actually gets logged to the console:

    <span>span inside</span>

    This surprisingly returns the <span> from inside the <p>.

    Why does this happen? MDN explains it like this:

    […] selectors is first applied to the whole document, not the baseElement, to generate an initial list of potential elements. The resulting elements are then examined to see if they are descendants of baseElement.

    Note: element.querySelectorAll() also applies selector scoping in the same manner.

    If we want to restrict the selector to the element on which it is called then we should use the :scope pseudo-class.

    const scopedQuery = baseElement.querySelector(':scope div span');
    console.log(scopedQuery); // Returns null, as expected

    As mentioned in the MDN docs for :scope, this works because:

    When used within a DOM API call — such as querySelector(), querySelectorAll(), matches(), or Element.closest():scope matches the element on which the method was called.