Skip to main content
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Katie Maher
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Katie Maher

Previous / Next <CSS Selector> Both Use A Depth-First Algorithm In HTMX

By
Published in ,

In the HTMX JavaScript framework, the hx-target attribute can reference other elements in the DOM (Document Object Model) by using CSS selectors, convenience keywords (such as this and next), and a combination thereof. Most of these are relatively straightforward; but, I ran into surprising results when using previous {CSS selector} and next {CSS selector}. I had read the documentation and interpreted it as meaning that the targeted elements were direct descendants of the same parent element. But, this is incorrect. Internally, HTMX is using a depth-first traversal algorithm—from the document root—to find the target nodes; and then, selects the one closest to the trigger element based on a depth-first node traversal order.

This is simple to demonstrate. We can nest a bunch of <section> elements and then see what happens to a button that uses previous section as its target:

<cfoutput>
<section>
<section>
<section>
<section>
<section>
Hello
</section>
</section>
</section>
</section>
</section>
<button
hx-get="more.cfm"
hx-target="previous section">
Replace "previous section"
</button>
</cfoutput>
view raw index.cfm hosted with ❤ by GitHub

Based on my interpretation of the HTMX documentation, when this button uses hx-target="previous section", I had assumed it was going to replace the contents of the top-most <section> element. However, when we run this ColdFusion code and click on the button, we see the following:

As you can see, it's actually the inner most <section> element that gets updated. This is actually the element farthest away from the button in terms of the DOM branching; but, it's the closest to the button in terms of DOM traversal order.

The next {selector} works the same way; which means that it may select a deeply nested target before it selects a sibling element. We can see this in the following ColdFusion page:

<cfoutput>
<button
hx-get="more.cfm"
hx-target="next section">
Replace "next section"
</button>
<div>
<div>
<div>
<section>
Hello
</section>
</div>
</div>
</div>
<section>
There
</section>
</cfoutput>
view raw index-2.cfm hosted with ❤ by GitHub

In this code, we have a deeply-nested <section> (inside some <div> elements) and a sibling <section> on the same level as the trigger button. And, when we try to target next section, we end up targeting the deeply nested one:

As you can see, both the previous {selector} and the next {selector} in HTMX use the depth-first node order as their measure of "closest".

The other point to take away from this is that the underlying .querySelectorAll() is being performed from the document root. Which means that the target of the previous {selector} and next {selector} don't even have to be in the same branch of the DOM tree—they only need to be "close" to each other in terms of a depth-first traversal.

Consider this small ColdFusion page:

<cfoutput>
<p>
<button hx-get="more.cfm" hx-target="next mark">
Replace "next mark"
</button>
</p>
<p>
<mark>Hello</mark>
</p>
</cfoutput>
view raw index-3.cfm hosted with ❤ by GitHub

In this code, the <button> is targeting a <mark> that's actually a cousin of the trigger element. But, again, based on a depth-first traversal, it's the "next" <mark> relative to the button.

This is actually feature of HTMX because it gives me the ability to target "near elements" without being super tightly coupled to the DOM structure. I'm glad I stumbled over my misunderstanding so that I can start to use these special selectors in the way they were intended to be used.

Want to use code from this post? Check out the license.

Reader Comments

Post A Comment — I'd Love To Hear From You!

Markdown formatting: Basic formatting is supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.
Cancel
I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel