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 - 1);
nest(_el, cls, n - 1)
}
_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.
I stopped reading when i saw you using double quotes in php code.
Hey this was pretty cool! Just note you’ve got some infinite recursion in your Javascript function when you add the second nest call:
Fixed, thanks!
While I have used Pug before, it somehow never came to me how much you may be able to simplify repetitive SVG markup. But once the idea is in my head…
One of these days, we’ll have to do a Code Golf battle CSS against SVG. Creativity and coding time, you will win straight out. But number of lines, I challenge you.
For the spinner
you in pure CSS has 6 lines of Pug and 32 lines of SCSS
me in pure SVG has 20 lines of Pug and 9 lines of Sass
See the Pen
jOrMjLW by ccprog (@ccprog)
on CodePen.
That is without setting precise rules. I think what we are both following is “no minification, no lines longer than 80 characters”.
I have just tried to devise a ruleset for web dev code golf: Sandbox for Proposed Challenges
I suspect that like me most of you won’t have seen much of this StackExchange community, but I am going there because of the specialists for formulating good rulesets, independent of language background. But I would appreciate input from the wider frontend community. Since the proposal sits in an answer, improvements are probably meant to be made in comments. For a description what this Sandbox is about, see its question.
That’s an interesting idea.
I’m thinking that if I were to go for SVG, I’d go for something like this, so I don’t have repetition in the compiled code either*. I suppose it’s a little bit more code to write than in your example, but it results in a significantly smaller SVG (super frustrating
pathLength
doesn’t work unless I set it on eachcircle
because that’s one source of repetition that really scratches my retina).*Funny enough, I actually started relying on Pug more heavily in order to simplify the compiled code once CSS variables became a thing. The preprocessor code was already pretty compact since looping was what first got me to start using HTML and CSS preprocessors back in 2013, but with variables, I could generate a bunch of elements using Pug, give them an index set as a custom property inline and then make CSS property values depend on those indices, instead of generating an
nth-child
block for each of the elements from the Sass.If you are also taking compiled length into account, here is an optimized, mixed version. I freely admit the CSS transform syntax is shorter, so I integrated it, and further optimized the way
<use>
is utilized.The new numbers are (no minification applied for the bytes):
yours: Pug 6 lines, SCSS 32 lines, sum: 40 lines
compiled HTML 129 bytes, CSS 1744 bytes, sum: 1873 bytes
mine, first version: Pug 20 lines, Sass 9 lines, sum: 29 lines
compiled HTML 2563 bytes, CSS 127 bytes, sum: 2690 bytes
mix: Pug 15 lines, CSS 14 lines, sum: 29 lines
compiled HTML 736 bytes, CSS 266 bytes, sum: 1002 bytes
I am so sorry, I missed the second version of yours
your pure svg: 17 lines + 12 lines = 29 lines
861 bytes + 219 bytes = 1080 bytes
Will be problems with older browsers?
Wasn’t Emmet developed for that? What am I missing?
I definitely don’t understand why you didn’t used Emmet.
In VSCode there is a gracious example (assuming some reader here is a beginner or still using Notepad++), in an extension Emmet Live. You could write just a sentence, press Tab and have already your full code thrown in html. No need for any php whatsoever. Things are already made simple, why to reinvent the wheel?
Other than that, your animations are really nice ;) you put a nice example for which you deserve a thumbs up!
Thanks!
First off, because I’m not even really sure what Emmet is. When I first heard people talk about it, I thought it was a text editor, but now I believe it’s an extension you can add to different IDEs from what you’re writing here? Anyway, not my thing.
For one, me and IDEs, we don’t get along. Didn’t get along back when I wrote C/ C++, Java, PHP, Lisp and so on… still not getting along and we won’t get along. All through the 2002- 2011 decade, I used to have post-its on my monitor with instructions from my more savvy friends and I still barely got by (translation: I was never any good and never managed to do much).
It was life-changing a decade ago to discover online playgrounds where all you had to do was write code in a box and press a run button and you could see the result of your code. No more needing three weeks before someone finally figures out what you fucked up with the damn IDE and is making you unable to run anything. No more seeing a zillion buttons menus and options that confuse you. No more stuff appearing/ disappearing/ getting messed up in any other way because you accidentally pressed some key or key combo that you don’t even know what it was or how to reverse without losing your work.
I may use the basic Text Editor that Ubuntu comes with from time to time, but that also has the advantage of having no crazy features that would only get in my way.
Anyway, from what I understand, you mean why not use an extension that would throw the full generated nested HTML in the code? I wouldn’t want to see that. In the code I work with, I’d rather see a recursive call instead of what that generates just planted there. It’s easier to understand, vertical space on my screen is limited and I find having to scroll through my code really confusing, so the shorter I can keep it, the better.
As for the animation part, I’m kind of scratching my head at that since animation is the most trivial thing in those demos. Except for maybe the last one, where things are a bit complicated by no browser supporting CSS trigonometric functions at this point, but I figured that since I used trigonometric functions to emulate CSS timing functions from the JS, I could do the opposite in the CSS.