Which CSS selector for my nested sections? (reloaded)

Previously on the CogitActive Saga:
It didn’t take long to find a simpler approach. Smarter. Semantic. Just use descendant selectors: section section for level two (i.e., my <h3>), section section section for level three (i.e., <h4>), and so on. No classes to add when encoding my post. No extra markup. Just pure structure.

Introduction

Before the concluding post of the series was abruptly derailed by my long courtroom drama over padding‑left versus margin‑left, I had already taken a decisive step. My mind was set on CSS selectors as the key to handling my nested sections. Almost instinctively, I leaned on the descendant selectors approach, convinced it was both clever and elegant. No classes to clutter the markup, no attributes to maintain — just the pure rhythm of structure itself. section section for level two, section section section for level three, and so on. A semantic staircase, rising neatly with each heading. For a moment, I believed I had found the perfect solution.

section section {
  padding-inline-start: 10%;
}

section section section {
  padding-inline-start: 20%;
}

section section section section {
  padding-inline-start: 30%;
}

section section section section section {
  padding-inline-start: 40%;
}

But confidence can be deceptive. When I finally implemented the code — yes, shame on me for not testing it earlier — the illusion shattered. I realized my mistakes, not one but many, and that was the moment the CSS Hydra revealed itself. The selectors I had trusted so quickly were not only failing to deliver the clean hierarchy I had envisioned, but I was also using them wrongly. What looked so orderly on paper dissolved into chaos in practice. Instead of a steady progression — 10 %, then 20 %, then 30 % — the indents ballooned, compounding with each level until the staircase collapsed into a crooked climb. This was the “failing to deliver” part. As for the “using them wrongly,” read on.

While I managed to untangle most of my mistakes later — and laid them bare in the following post, Through CSS confusion toward insight — another realization lingered in the background. I had been too hasty in my decision to embrace these selectors, too eager to declare victory before truly testing their behavior. What once seemed like a semantic triumph now revealed itself as a fragile shortcut. The time has come to rethink them entirely, to strip away the assumptions and rebuild the logic from the ground up. And, as promised, to finally confront the first hurdle that exposed the flaw in my approach: why 1+2=3.

Descendant selectors

In that post, I recounted how my neat staircase of indentation collapsed under the weight of cumulative application. As alluded to in the introduction, what should have been a steady rhythm — 10 %, then 20 %, then 30 % — ballooned into 30 %, 60 %, and beyond. The staircase I had envisioned became a crooked climb, each step wider than the last. That was problem #1. Fortunately, I could easily fix it by changing the previous code into this:

section section {
  padding-inline-start: 10%;
}

section section section {
  padding-inline-start: 10%;
}

section section section section {
  padding-inline-start: 10%;
}

section section section section section {
  padding-inline-start: 10%;
}

At first, I thought I had uncovered the reason behind the ballooning indents. My flawed reasoning went like this: if section section section applied the intended indent to the h3 level, while section section applied to them all — h2 and h3 alike — then the h3 would inherit both. Two plus one equals three. In other words, the indents were stacking because the selectors overlapped, and the deeper sections were receiving multiple increments at once.

For months, I carried the conviction that padding preserved the width of the parent container, enclosing the indent inside the box without altering its dimensions. In my mind, padding created towers: each child stacked neatly beneath its parent, the blocks aligned and stable. But the truth — unveiled in my long‑awaited Verdictwas different. A child section is not a full block standing beside its parent; it is a block constrained by the parent’s content area. That subtle distinction changes everything.

The first implication is shrinkage. Because each parent’s padding reduces its own content width, the child inherits that narrower space. With every new level of nesting, the available width contracts further, and the percentages that once seemed stable begin to wobble. What should have been a consistent 10 % indent becomes a moving target, compounding downward. That was my second hurdle — “Hurdle #2: 23.594 ≠ 22.351” — but I have already addressed this in the previous post.

More importantly, this geometry explains the riddle of 1+2=3. A child section sits inside the content area of its parent; not as a full block standing beneath it, but as a block constrained by the parent’s reduced width. And because it lives inside that space, it also carries the parent’s padding on its left. The arithmetic is simple: first the parent’s 10 % (“1”), then the child’s 20 % (“2”), together yielding 30 % (“3”). The staircase does not rise in neat increments; it balloons, each step wider than the last, because the padding of every ancestor accumulates.

Each new heading carried the weight of the previous padding with it.

This has nothing to do with my earlier, mistaken explanation about overlapping selectors. The culprit is not selector overlap but the very behavior of padding itself. And yet, I was not completely wrong. The selector section section does indeed target them all. Every nested section, regardless of depth, qualifies as a descendant of another, and so this single rule is enough to reach them. In other words, instead of the verbose cascade of selectors I had written, I could have simply used:

section section { 
  padding-inline-start: 10%; 
}

Child combinator

It was still during that long, restless night — the one spent fighting the Hydra, too tired to think clearly yet too stubborn to stop. That was the night when my biggest mistake — trusting Copilot — reached its climax. When I asked why my indents were ballooning (see above), it offered a solution under the banner of “Ways to get the behavior you likely want.” The recommended approach was to switch to the child combinator: section > section. According to Copilot, this would give me “precise per‑level, non‑cumulative spacing.” I had seen this selector before, tucked away in documentation and examples, but dismissed it without much thought. Now, in the haze of fatigue, it resurfaced as a lifeline. And so, I tried it.

To make a long story short — the full version is already laid out in Through CSS confusion toward insight as my Hurdle #2 — the child combinator turned out to be no savior. First, it didn’t work as expected, and I eventually discovered why: Gutenberg had slipped in extra <div> wrappers, breaking the neat parent‑child logic I thought I was applying. Second, even when I tried to force the approach (see below), the solution failed outright. The indents ballooned just as they had with the descendant selectors, because the problem was never the selector at all (see above). And third, Copilot assured me that unlike with the descendant selector — where section section alone would be enough to target every nested level — this would not hold true here. Guess what? All wrong, again. It does.

According to these observations — my conclusions finally cleaned of Copilot’s misdirections — I realized I could still have used the child combinator approach. If I were to trust Copilot’s logic (I know…), this version was the “better” path: it narrowed the scope to direct children while accounting for Gutenberg’s extra <div> wrappers. In practice, the selector looked like this 1:

section > .wp-block-group__inner-container > section {
  padding-inline-start: 10%;
}

This rule would have applied the indent precisely at each level, bypassing the structural noise Gutenberg had introduced. But — because there is always a “but” — the solution carried its own fragility 2. If WordPress were ever to change that class name or alter the structure of the wrapper <div>, my carefully tailored selector would collapse, and the code would break without warning. Now, I could have tried this instead:

section > * > section {
  padding-inline-start: 10%;
}

But that solution is dangerously broad. What if a plugin introduces its own markup using <section> elements? Imagine a contact‑form plugin that wraps each field group inside a <section> for styling. My rule would blindly indent those sections as well, even though they have nothing to do with my content hierarchy. The result would be a form pushed awkwardly to the right, its layout broken by a selector that was meant only for headings.

In the end, the child combinator was no more reliable than its descendant cousin. Between Gutenberg’s unpredictable wrappers, WordPress’s shifting class names, and the risk of plugins hijacking the markup, the approach proved too fragile to trust. What looked like a lifeline in the haze of fatigue turned out to be another dead end.

What else?

After that epic night fighting the Hydra — and before I finally admitted to myself that I should never ask Copilot for CSS advice — I went back to it once more, trying to make sense of the hurdles I had faced. As you already know, the answers didn’t come from Copilot, but from me, once I unplugged it and started thinking on my own.

Relational selector

One of the first solutions it offered was the relational :has() selector. The idea was to target <section> elements that contain an <h3> heading, like so:

section:has(h3) {
  padding-inline-start: 10%;
}

At first glance, this seemed to address the concerns I had raised earlier. By focusing only on sections that actually contained headings, the rule avoided the pitfalls of relying on Gutenberg’s wrapper classes. It promised a cleaner, more semantic approach: the selector itself expressed the intent — “indent sections with headings” — rather than depending on structural quirks. But, as with every supposed lifeline in this series, the approach was not without caveats…

Since December 2023, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.MDN Web Docs

Falling into the rabbit hole

So, as alluded to earlier, I finally unplugged Copilot and began to think on my own. Or rather, I started where I should have from the very beginning: by reading about CSS selectors, learning everything I could — the CogitActive way — before daring to test what might be the best approach.

The CSS selectors (external link) page opened before me like a map of hidden kingdoms. Suddenly, I was staring at a world of limitless possibilities, a toolbox brimming with selectors I had never imagined. Each one promised a new way to reach into the structure of my nested sections, to bend them, shape them, target them with precision. It was exhilarating, but also overwhelming. To master this science — or was it art? — would take me eons.

I knew I could not afford to delay this mini‑series (no longer so “mini,” truth be told) any further. But it was already too late. I had fallen into the rabbit hole. My brain was racing ahead, conjuring ideas I could barely comprehend. I even began to wonder if I could invert my logic entirely: instead of moving from parent to child (section > * > section), perhaps I could find a way to target a section based on something about its parent.

I kept digging, deeper and deeper, until — fortunately — a sudden revelation struck me, sharp and undeniable.

Back to the good old basics

From the very beginning, I had turned my back on classes. Not out of laziness, but out of caution: what if, in the haze of a late night after a long day’s work, I simply forgot to add one? As faithful readers of this blog know, I code my posts directly in HTMLGutenberg may lend a hand, but the structure is mine to shape. And when fatigue dulls my attention, the risk of omission grows. I wanted a system that lived entirely in the markup itself, a way to target my nested sections without relying on memory or vigilance. Pure structure, self‑sufficient, immune to human forgetfulness.

And yet, the revelation came suddenly, almost by accident. As I prepared the next post in the series — my hopefully final attempt to resume the interrupted one — I realized it would begin not with an H2, but with an H3. A deliberate break in the semantic staircase, resuming exactly where the interruption had left off. That single choice shattered the illusion. No selector, however clever, could rescue me from the collapse of structure.

In that instant, the truth was undeniable: the very tool I had rejected from the start, the humble class, was the safer path. The class was not a clever trick, nor a glimpse of genius, but the steady anchor I should have trusted from the start. Actually, it is not only safe, but reliable — a steady companion that never falters when the structure shifts. It is robust, standing firm against the unpredictable wrappers of Gutenberg and the whims of plugins. It is maintainable, offering clarity and ease when the time comes to adjust or refine. It is scalable, ready to grow with the content, no matter how deep the nesting or how wide the layout. And above all, it is future‑proof, immune to the fragile tricks that had failed me before.

Safe. Robust. Recommended.

Of course, this choice is not without its cost. The danger remains that I might forget to add the class — a single omission, and the indentation collapses, leaving the structure crippled. There is also the extra step in Gutenberg: a small detour in my workflow, though hardly a burden. I do not even need to add classes to my H2 sections, only to the nested ones — precisely the place where forgetfulness lurks. And yes, I must go back retroactively to the few posts already published, stitching the classes into their markup. But these are minor inconveniences, tiny tolls on the road compared to the benefits.

So, here it was — a line of code I could have written from day one, had I not been so stubborn:

.nested {
  margin-inline-start: 10%;
}

Yes, that’s it. After all the detours, the Hydra battles, the courtroom debates, the semantic staircases — this humble snippet was sitting there, waiting patiently, while I wandered in circles. Sarcastic? Perhaps. But not fruitless. Because even if I return to square one (or zero, if we’re honest), the journey itself mattered. As the saying goes, it’s not the destination that counts, but the path. And along that path, even interrupted, even scratching only the surface of CSS selectors, I learned something new:

section.nested {
  margin-inline-start: 10%;
}

This small refinement makes all the difference. By anchoring the class to the section element, the intent is explicit: it is not just any .nested, but a nested section. The selector gains clarity, precision, and resilience. It avoids accidental collisions with other elements that might share the same class name, and it ties the styling directly to the semantic structure I care about.

And so, after all the wandering, I arrive back at the good old basics — but wiser, sharper, and with code that is both simple and strong. The class is not glamorous, but it is safe, reliable, and recommended. And sometimes, after nights of struggle and months of delay, the most enduring insight is that the old, steady tools were enough all along.


1 On the use of section > section > section: I could have made this work — applying 10 % at each level to account for the cumulative effect — by inserting the class path between every step. But that would have been far too verbose, and besides, what is the point when a single rule can target them all? ^
2 Moreover, the danger here is that Gutenberg uses this class (wp-block-group__inner-container) for any and every group, not just my sections. That broad application makes it too risky to rely on; one change in WordPress or one plugin using groups differently could break the layout entirely. ^