Fun with CSS Selectors
How to Target Boundary Elements in a DOM Subtree
Published 11/11/2024
Contents
1 Problem statement
The layout of content on this website was partly inspired by the work of Edward Tufte, who uses sidenotes extensively. Sidenotes are like footnotes, except they don’t force the reader to jump their eye to the bottom of the page, but instead display off to the side in the margin[1][1]: This is a sidenote..
Sidenotes can contain arbitrary content, including block and inline
elements such as figures and equations. To position sidenotes, I wrote a
dynamic sidenote positioning algorithm (in JavaScript), which attempts
to position the sidenotes as close to their corresponding links as
possible. Due to their arbitrary content, it can be a challenge to line
up the tops of the sidenote content with the corresponding link, as the
sidenote content’s first elements usually have non-zero
margin-top values by default. Therefore, we need to set
margin-top: 0 on those elements. Assuming that sidenotes
may be represented by the class sidenote[2], this seems like a simple task:
The sidenote class is used here only for illustrative
purposes, and is not actually in use on this website. The actual
sidenote selector is stated below:
:is(span[role='note'],
div[role='note'],
aside) { ... }
However, to simplify the matter, the .sidenote class
shall be used instead.
.sidenote > *:first-child {
margin-top: 0;
}
Equally, the bottom margin of the last sidenote content element must also be set to zero, so that the sidenote doesn’t take up more space than it has to:
.sidenote > *:last-child {
margin-bottom: 0;
}
However, if the sidenote’s last child itself has a last child with a non-zero bottom margin, this won’t work. Consider the situation in Fig. 1:
In Fig. 1, both the
<figure>’s as well as the
<figcaption>’s bottom margins need to be set to zero.
This could be achieved with two separate selectors:
.sidenote > *:last-child {
margin-bottom: 0;
}
.sidenote > *:last-child > *:last-child {
margin-bottom: 0;
}
While this will likely cover >99% of cases, it won’t work if the
<figcaption> again contains a last-child
with a non-zero bottom margin. In that case, we would have to add
another selector, and so on. It becomes evident that the above strategy
doesn’t work in general and is also not very elegant. The task is
therefore to find selectors that target all left and right boundary
elements of the DOM subtree that is the sidenote and its content
respectively.[3][3]: These nodes
are highlighted in blue and red below. The sidenote is the parent
element of the first red and the first blue element (when traversing the
tree top down).
2 Solution
The solution turned out to be the selectors below:
.sidenote *:first-child:not(.sidenote *:not(:first-child) *) {
margin-top: 0;
}
.sidenote *:last-child:not(.sidenote *:not(:last-child) *) {
margin-bottom: 0;
}
The first selector targets all descendents of the sidenote which are first children and which do not also descend from descendents of the sidenote that are not first children. The bottom selector works the same way, but considers last children instead.