Deep Nesting Without the Hand-Written Hell

Generating a deeply nested HTML structure by hand is tedious, and it only gets worse when you’re doing it through a preprocessor or templating language. Hand-writing every level is not just error-prone; it also makes the codebase harder to maintain. The better path is recursion. Whether you’re working with Haml, Pug, JavaScript, or PHP, a recursive mixin or function keeps the markup clean and scalable.

<div class='boo'>
  <div class='boo'>
    <div class='boo'>
      <div class='boo'>
        <div class='boo'></div>
      </div>
    </div>
  </div>
</div>

For Haml, a recursive mixin solves the problem neatly. Pug can do the same with a mixin that calls itself. In JavaScript, you can build the DOM nodes recursively, and in PHP, a simple function that echoes nested divs works fine. The only meaningful difference between these approaches is whitespace and formatting; for instance, targeting the innermost element with .👻:empty works with Haml, JavaScript, and PHP output, but not with Pug due to how Pug handles whitespace and nested element boundaries.

Adding a Level Variable

If you want each nested element to carry its own depth index, you can set a custom property like --i on each element. That becomes useful for styling gradients or backgrounds that shift per level. A common alternative is filter: hue-rotate(), but that affects saturation and lightness too, and it doesn’t give you the same degree of control as a plain custom property.

With Pug, adding a level counter is just a matter of incrementing a parameter in the recursive call. Haml follows the same pattern. In JavaScript, you pass the level number into your DOM-creation function. PHP likewise can track depth by adding a parameter to the recursive nest() call.

Branching into a Tree

Instead of a straight chain, each node can have two children, forming a binary tree. The recursive mixin barely changes. For Pug, you just call the mixin twice inside itself. Haml needs very little modification as well. JavaScript requires a bit more code because you need to create and append two child nodes per parent, but it’s still compact. PHP only needs one extra recursive call inside the conditional block.

// PHP recursively building a tree
function nest($n) {
    if ($n > 0) {
        echo '<div class="boo" style="--i:' . $n . '">';
        nest($n - 1);
        echo '</div>';
    } else {
        echo '<div class="boo" style="--i:' . $n . '"></div>';
    }
}

Styling the top-level node differently can be done purely in CSS, without adding extra classes, by targeting the direct children of the container.

Be Mindful of Inherited Effects

Deep nesting has a subtle gotcha: properties like transform, filter, clip-path, mask, and opacity affect not only the element they are applied to, but all its descendants too. Sometimes that is exactly what you want, and it is why nesting these elements makes sense over making them siblings. But when you need to reverse a parent’s effect on a child, you are out of luck for most of these; you cannot compensate for a parent opacity: .8 by setting the child to opacity: 1.25.

Real-World Uses

Recursive nesting shows its worth in animated CSS demos. A pure CSS dot loader, for example, relies on scaling transforms and animated rotations whose effects accumulate on inner elements, as do opacities. A yin-and-yang dance uses a tree structure where each child is half the size of its parent, and the innermost leaves get extra gradient layers; rotations from all ancestors add up on the leaves as well.

Tentacle-like spinning rings demonstrate stacking rotations across several nesting depths. And a triangular openings demo takes advantage of individual transform properties scale and rotate to apply different timing functions per property. That demo requires the Experimental Web Platform features flag in chrome://flags for Chromium browsers.

Triangular openings (live demo).

The nesting mixin is slightly modified here to also set a per-level color, showing how easy it is to extend the recursive pattern beyond just structural wrappers.