Tailwind Typography Lost Its Paragraph Spacing
An unlayered CSS reset quietly beat Tailwind Typography's prose styles; cascade layers explain why, and one @layer block fixes it.

For a while, every article on this blog was one long paragraph in disguise. The Markdown had blank lines. The HTML had separate <p> elements. The page put no space between them anyway. Numbered lists lost their numbers, and links looked exactly like the text around them, which is a brave choice for a link.
The site runs Astro with Tailwind CSS v4, and the article body uses the typography plugin’s prose class. Handling paragraphs, lists, and links is the entire job of prose. So either the plugin was broken, or it was losing an argument.
Ask what won, not what you wrote
The stylesheet tells you what you asked for. The computed style tells you what the browser actually decided. In the console on an article page:
const p = document.querySelector('.prose p');
const ol = document.querySelector('.prose ol');
const a = document.querySelector('.prose a');
({
margin: getComputedStyle(p).marginTop,
list: getComputedStyle(ol).listStyleType,
link: getComputedStyle(a).textDecorationLine,
});
{ margin: '0px', list: 'none', link: 'none' }
Three different properties, all reset to nothing. That isn’t one stray override. That’s a reset stylesheet doing its job on the wrong page.
The culprit was a design-system file I had vendored into the project and imported after Tailwind. Simplified:
@import "tailwindcss";
@import "./design/base.css";
@plugin "@tailwindcss/typography";
/* design/base.css */
p, h1, h2, h3, h4, h5, h6 { margin: 0; }
ul, ol { list-style: none; margin: 0; padding: 0; }
a { color: inherit; text-decoration: none; }
Why a bare p beat .prose p
By specificity, it shouldn’t. The plugin’s paragraph selector is .prose :where(p) plus a :not() guard, which beats a plain p.
But specificity only settles ties inside the same cascade layer. Tailwind v4 is built on native layers. Its entry file starts with:
@layer theme, base, components, utilities;
In the built CSS, the prose rules sit inside utilities. The vendored reset sat in no layer at all. Per MDN, unlayered styles override everything declared inside layers, and once layers are involved, specificity between them stops mattering.
Think of layers as piles of rules the browser reads in order. Unlayered CSS is a sticky note on top of the last pile. Specificity decides fights within a pile. It never lets a rule climb out of one.
The fix: put the reset in its pile
@layer base {
p, h1, h2, h3, h4, h5, h6 { margin: 0; }
ul, ol { list-style: none; margin: 0; padding: 0; }
a { color: inherit; text-decoration: none; }
}
Tailwind’s own Preflight already lives in base, so the reset now sits next to the reset it imitates, underneath utilities. Same console check:
{ margin: '23.625px', list: 'decimal', link: 'underline' }
That margin is 1.25em at an 18.9px font size. prose is back in charge.
The fights it had been winning quietly
Moving the reset didn’t only free prose. The same file also set body { font-family: Poppins }. Elsewhere, an @layer base rule set the body to Lora. That rule had never once applied, because the unlayered Poppins rule always won.
Once both rules lived in base, normal source order applied. The later rule won, and the whole interface switched to a serif font. The cascade was working perfectly, and that was the problem. The fix was to set the body font on purpose, in one place.
Before you merge a change like this, search your templates for utilities on the affected elements (mt-* on a p, list-* on a ul, underline on an a). They were silently overruled before. They’ll start working the moment the reset moves into a layer.
While you’re in there: the backticks
With prose working again, inline code suddenly showed literal backticks around it. That’s the plugin’s default: code::before and code::after each add a `. On code that already has a background chip, they’re just noise:
.prose code::before,
.prose code::after {
content: none;
}
This rule lives in an unlayered stylesheet on purpose, because it has to beat a utility. This time, an unlayered rule winning is the whole point.
If you see this
proseparagraphs reportmargin-top: 0px: look for bare element selectors (p,ul,a) in unlayered CSS, vendored files included.- DevTools shows the
.prosedeclaration crossed out by a less specific rule: you’re looking at layer order, not specificity. - Fonts or spacing change elsewhere after you add
@layer: a rule the reset used to beat is finally getting its turn.
References
- MDN: @layer
- Tailwind CSS: Adding custom styles
- Tailwind CSS source: index.css (layer order)
- @tailwindcss/typography README
Cover photo: Unsplash.