← all posts · css

The CSS specificity trap that killed my paragraph spacing

How a routine margin reset overrode the owl selector and made all my prose paragraphs run together — and the one-line fix.

04 May 2026 · 4 min read · Stephen Masters css

I was looking at a freshly styled blog post and something felt wrong. The text was readable, the line height was fine, but the paragraphs looked wrong — like there was no gap between them. There was a gap, technically, but it was the same as the gap between lines in the same paragraph. The page felt like one continuous block of text.

The layout had looked fine in the mockup. Something had broken it when I wired it up to real content.


The setup

The prose styles were built on the owl selector — a pattern for adding spacing between sibling elements without touching individual components:

css
123
.prose > * + * {
  margin-top: var(--s-5); /* 24px */
}

This adds a top margin to every direct child of .prose that follows another child: headings after paragraphs, paragraphs after headings, blockquotes, code blocks, all of it. One rule, no element-specific exceptions.

There was also a reset to kill the browser’s default paragraph margin:

css
123
.prose p {
  margin: 0;
}

Browsers add margin-block-start and margin-block-end to <p> elements by default. If you don’t zero them out, they stack with whatever spacing your design adds, and you get gaps that are slightly too large and inconsistent across browsers.

So: owl selector adds spacing, margin reset kills the browser default. Except it also killed the owl selector’s spacing. Every <p> inside .prose had margin-top: 0, full stop.


Why it happened

CSS specificity is calculated as three columns: ID selectors, class/attribute/pseudo-class selectors, and element/pseudo-element selectors. A higher number in any column beats a lower one to its left.

RuleIDsClassesElementsSpecificity
.prose > * + *010(0, 1, 0)
.prose p011(0, 1, 1)

.prose p has one more element selector than the owl selector, so it wins — regardless of which rule appears later in the source. Both rules target the same <p> element inside .prose. The reset wins, and the owl selector’s margin-top is overridden.

The common misconception is that source order is what matters. It does, but only as a tiebreaker when specificity is equal. Here they’re not equal, so source order is irrelevant.


The fix

Add a third rule that is more specific than the reset, and only fires between adjacent paragraphs:

css
123
.prose p + p {
  margin-top: var(--s-6); /* 32px */
}
RuleIDsClassesElementsSpecificity
.prose > * + *010(0, 1, 0)
.prose p011(0, 1, 1)
.prose p + p012(0, 1, 2)

.prose p + p wins over both. The reset still kills the browser default margin on every <p> (which is what it’s there for), and the p + p rule re-adds spacing only between consecutive paragraphs — which is exactly the case the owl selector was supposed to handle.

I used --s-6 (32px) rather than the owl selector’s --s-5 (24px) to give paragraph breaks a bit more weight than other element transitions. Paragraphs after paragraphs need a clearer visual break than, say, a paragraph after a heading. That distinction was there in the original design and was worth preserving.


The general lesson

The “reset to zero, then re-add where needed” pattern is common in CSS. It’s a sensible approach — clear out browser defaults, then apply your own spacing intentionally. The trap is when the reset selector is more specific than the rule that re-adds spacing.

Before writing element { margin: 0 }, check what selectors are responsible for adding that margin back. If the re-add rule has lower specificity than the reset, the re-add will silently lose every time, and you’ll spend a while wondering why the spacing you thought you defined isn’t showing up.

The owl selector in particular is vulnerable to this: it’s deliberately low-specificity (one class selector, two universal selectors) so it doesn’t get in the way. Any element-level reset inside the same scoping class will outrank it.

SM
Stephen Masters

Software developer and architect. I build systems for places that move energy, commodities, and money around. I keep a bike-packing journal at velostevie.com.