Smarter Ways to Generate a Deep Nested HTML Structure
Let’s say we want to have the following HTML structure:
<div class='boo'>
<div class='boo'>
<div class='boo'>
<div class='boo'>
<div class='boo'></div>
</div>
</div>
</div>
</div>
That’s real a pain to write manually. And the reason why this post was born was being horrified on seeing it generated with Haml like this:
.boo
.boo
.boo
.boo
.boo
There were actually about twenty levels of nesting in the code I saw, but maybe some people are reading thing on a mobile phone, so let’s not fill the entire viewport with boos, even if Halloween is near.
As you can probably tell, manually writing out every level is far from ideal, especially when the HTML is generated by a preprocessor (or from JavaScript, or even a back-end language like PHP). I’m personally not a fan of deep nesting and I don’t use it much myself, but if you’re going for it anyway, then I think it’s worth doing in a manner that scales well and is easily maintainable.
So let’s first take a look at some better solutions for this base case and variations on it and then see some fun stuff done with this kind of deep nesting!
The base solution
What we need here is a recursive approach. For example, with Haml, the following bit of code does the trick:
- def nest(cls, n);
- return '' unless n > 0;
- "<div class='#{cls}'>#{nest(cls, n - 1)}</div>"; end
= nest('👻', 5)
There’s an emoji class in there because we can and because this is just a fun little example. I definitely wouldn’t use emoji classes on an actual website, but in other situations, I like to have a bit of fun with the code I write.
We can also generate the HTML with Pug:
mixin nest(cls, n)
div(class=cls)
if --n
+nest(cls, n)
+nest('👻', 5)
Then there’s also the JavaScript option:
function nest(_parent, cls, n) {
let _el = document.createElement('div');
if(--n) nest(_el, cls, n);
_el.classList.add(cls);
_parent.appendChild(_el)
};
nest(document.body, '👻', 5)
With PHP, we can use something like this:
<?php
function nest($cls, $n) {
echo "<div class='$cls'>";
if(--$n > 0) nest($cls, $n);
echo "</div>";
}
nest('👻', 5);
?>
Note that the main difference between what each of these produce is related to formatting and white space. This means that targeting the innermost “boo” with .?:empty
is going to work for the Haml, JavaScript and PHP-generated HTML, but will fail for the Pug-generated one.
Adding level indicators
Let’s say we want each of our boos to have a level indicator as a custom property --i
, which could then be used to give each of them a different background
, for example.
You may be thinking that, if all we want is to change the hue, then we can do that with filter: hue-rotate()
and do without level indicators. However, hue-rotate()
doesn’t only affect the hue, but also the saturation and lightness. It also doesn’t provide the same level of control as using our own custom functions that depend on a level indicator, --i
.
For example, this is something I used in a recent project in order to make background
components smoothly change from level to level (the $c
values are polynomial coefficients):
--sq: calc(var(--i)*var(--i)); /* square */
--cb: calc(var(--sq)*var(--i)); /* cube */
--hue: calc(#{$ch0} + #{$ch1}*var(--i) + #{$ch2}*var(--sq) + #{$ch3}*var(--cb));
--sat: calc((#{$cs0} + #{$cs1}*var(--i) + #{$cs2}*var(--sq) + #{$cs3}*var(--cb))*1%);
--lum: calc((#{$cl0} + #{$cl1}*var(--i) + #{$cl2}*var(--sq) + #{$cl3}*var(--cb))*1%);
background: hsl(var(--hue), var(--sat), var(--lum));
Tweaking the Pug to add level indicators looks as follows:
mixin nest(cls, n, i = 0)
div(class=cls style=`--i: ${i}`)
if ++i < n
+nest(cls, n, i)
+nest('👻', 5)
The Haml version is not too different either:
- def nest(cls, n, i = 0);
- return '' unless i < n;
- "<div class='#{cls}' style='--i: #{i}'>#{nest(cls, n, i + 1)}</div>"; end
= nest('👻', 5)
With JavaScript, we have:
function nest(_parent, cls, n, i = 0) {
let _el = document.createElement('div');
_el.style.setProperty('--i', i);
if(++i < n) nest(_el, cls, n, i);
_el.classList.add(cls);
_parent.appendChild(_el)
};
nest(document.body, '👻', 5)
And with PHP, the code looks like this:
<?php
function nest($cls, $n, $i = 0) {
echo "<div class='$cls' style='--i: $i'>";
if(++$i < $n) nest($cls, $n, $i);
echo "</div>";
}
nest('👻', 5);
?>
A more tree-like structure
Let’s say we want each of our boos to have two boo children, for a structure that looks like this:
.boo
.boo
.boo
.boo
.boo
.boo
.boo
.boo
.boo
.boo
.boo
.boo
.boo
.boo
.boo
Fortunately, we don’t have to change our base Pug mixin much to get this (demo):
mixin nest(cls, n)
div(class=cls)
if --n
+nest(cls, n)
+nest(cls, n)
+nest('👻', 5)
The same goes for the Haml version:
- def nest(cls, n);
- return '' unless n > 0;
- "<div class='#{cls}'>#{nest(cls, n - 1)}#{nest(cls, n - 1)}</div>"; end
= nest('👻', 5)
The JavaScript version requires a bit more effort, but not too much:
function nest(_parent, cls, n) {
let _el = document.createElement('div');
if(n > 1) {
nest(_el, cls, n);
nest(_el, cls, n)
}
_el.classList.add(cls);
_parent.appendChild(_el)
};
nest(document.body, '👻', 5)
With PHP, we only need to call the nest()
function once more in the if
block:
<?php
function nest($cls, $n) {
echo "<div class='$cls'>";
if(--$n > 0) {
nest($cls, $n);
nest($cls, $n);
}
echo "</div>";
}
nest('👻', 5);
?>
Styling the top level element differently
We could of course add a special .top
(or .root
or anything similar) class only for the top level, but I prefer leaving this to the CSS:
:not(.👻) > .👻 {
/* Top-level styles*/
}
Watch out!
Some properties, such as transform
, filter
, clip-path
, mask
or opacity
don’t only affect an element, but also also all of its descendants. Sometimes this is the desired effect and precisely the reason why nesting these elements is preferred to them being siblings.
However, other times it may not be what we want, and while it is possible to reverse the effects of transform
and sometimes even filter
, there’s nothing we can do about the others. We cannot, for example, set opacity: 1.25
on an element to compensate for its parent having opacity: .8
.
Examples!
First off, we have this pure CSS dot loader I recently made for a CodePen challenge:
Here, the effects of the scaling transforms and of the animated rotations add up on the inner elements, as do the opacities.
Next up is this yin and yang dance, which uses the tree-like structure:
For every item, except the outermost one (:not(.??) > .??
), the diameter is equal to half of that of its parent. For the innermost items (.??:empty
, which I guess we can call the tree leaves), the background
has two extra radial-gradient()
layers. And just like the first demo, the effects of the animated rotations add up on the inner elements.
Another example would be these spinning candy tentacles:
Each of the concentric rings represents a level of nesting and combines the effects of the animated rotations from all of its ancestors with its own.
Finally, we have this triangular openings demo (note that it’s using individual transform properties like rotate
and scale
so the Experimental Web Platform features flag needs to be enabled in chrome://flags
in order to see it working in Chromium browsers):
This uses a slightly modified version of the basic nesting mixin in order to also set a color
on each level:
- let c = ['#b05574', '#f87e7b', '#fab87f', '#dcd1b4', '#5e9fa3'];
- let n = c.length;
mixin nest(cls, n)
div(class=cls style=`color: ${c[--n]}`)
if n
+nest(cls, n)
body(style=`background: ${c[0]}`)
+nest('🔺', n)
What gets animated here are the individual transform properties scale
and rotate
. This is done so that we can set different timing functions for them.
The post Smarter Ways to Generate a Deep Nested HTML Structure appeared first on CSS-Tricks.
You can support CSS-Tricks by being an MVP Supporter.