Skip to main content
luke@terminal:/blog$ ls
luke@terminal:/blog$ cat tailwind-typography-plugin-troubles.md

Installing @tailwindcss/typography Was Not The Solution I Needed

Created: 2025-12-09 | 4 min read

Note: This post was written before I redesigned my site with the terminal aesthetic. My color system has since changed from quantum-violet and automation-amber to terminal-primary and terminal-accent, and from neural-silver and code-carbon to terminal-dim and terminal-bg. The concepts and code are still valid, but the color names are outdated.

I had a simple problem: bullet points weren't showing up in my blog posts. The solution seemed obvious. Install @tailwindcss/typography, add the prose class, done. Except it wasn't done. It broke everything else.

My quantum-violet headings? Gone. My carefully styled inline code blocks? Illegible black-on-black text. The automation-amber links I spent time on? Also overwritten. The bullets appeared, sure, but at what cost?

Here's what I learned about Tailwind plugins, CSS specificity, and why sometimes the simple solution is better than the "official" one.

The Original Problem

I was rendering markdown blog posts in Next.js using Velite. The HTML was generating fine, but lists had no bullets:

<div dangerouslySetInnerHTML={{ __html: post.content }} />

The issue? Tailwind's base reset styles strip all default list styling. No bullets, no numbers, no margins. Just plain text where lists should be.

The "Official" Solution That Wasn't

Every search result pointed to the same answer: install @tailwindcss/typography and use the prose class. It's literally built for styling markdown content. Perfect, right?

npm install -D @tailwindcss/typography
@import "tailwindcss";
@plugin "@tailwindcss/typography";
<div
  className="prose prose-lg"
  dangerouslySetInnerHTML={{ __html: post.content }}
/>

And it worked! The bullets appeared. But then I actually looked at the page.

What the Typography Plugin Broke

1. Headings Lost Their Brand Color

I had spent time setting up my brand colors. quantum-violet for headings, automation-amber for links. All defined in my globals.css:

@layer base {
  h1, h2, h3 {
    @apply text-violet-400 mt-2 mb-2;
  }
}

The prose class overrode all of it. Headings were now default gray. My carefully crafted brand identity? Ignored.

2. Links Lost Their Brand Color Too

The same thing happened with my automation-amber links. I had specific link styling in my base styles, but the typography plugin came with its own --tw-prose-links color and underlined everything by default. Gone was my subtle hover effect. In its place: the plugin's opinionated blue-purple link scheme.

3. Inline Code Became Unreadable

I had custom styling for inline code. neural-silver text on a code-carbon background:

code {
  @apply bg-slate-700 text-slate-400 px-1.5 py-0.5 rounded;
}

The typography plugin decided code should have a light background with dark text. Except my site has theme variables, and in certain contexts, this created black text on a near-black background. Completely illegible.

4. The Plugin Added Its Own Opinions

The typography plugin adds backticks around inline code (content: "\"`), specific link colors, and a bunch of other opinionated styles. Great if you want the default prose styling. Not great if you've already built a custom design system.

Trying to Override the Plugin

My first instinct was to customize the prose styles through the plugin's CSS custom properties. The docs showed you could override defaults like --tw-prose-headings and --tw-prose-links. This seemed like the intended solution. Why else would they expose these variables?

I added to my globals.css:

.prose {
  --tw-prose-headings: #6b46c1;
  --tw-prose-links: #f59e0b;
  --tw-prose-code: #8b9dc3;
  --tw-prose-pre-bg: #1f2937;
}

Refreshed the page. Nothing changed. The variables weren't being picked up, or something else had higher specificity. My headings were still gray.

Next attempt: match the plugin's exact selector pattern. I inspected the computed styles and saw it was using complex :where() selectors. So I tried to beat it at its own game:

.prose :where(h1, h2, h3):not(:where([class~="not-prose"] *)) {
  color: #6b46c1 !important;
}

Still nothing. I moved it outside the @layer base block. I tried adding it directly to the component's style prop. I even looked at the plugin's source code to see what I was up against.

The plugin was winning every specificity battle. I was spending more time fighting it than I would have spent just writing the CSS from scratch.

The Realization

I was fighting the plugin. Customizing every single element to match my existing styles. Adding !important everywhere. Writing increasingly complex selectors.

And then it hit me: I don't need a plugin to add bullet points.

The typography plugin is solving a complex problem. Styling arbitrary markdown content when you don't have custom styles. But I do have custom styles. For everything except lists.

The Actual Solution

I removed the typography plugin entirely:

npm uninstall @tailwindcss/typography

And added exactly what I needed. List styles scoped to my article container. Since my blog posts render inside <article> tags anyway, scoping to article kept things contained:

@layer base {
  /* All my existing styles stay unchanged */

  article ul {
    list-style-type: disc;
    margin-left: 2rem;
    margin-top: 1rem;
    margin-bottom: 1rem;
  }

  article ol {
    list-style-type: decimal;
    margin-left: 2rem;
    margin-top: 1rem;
    margin-bottom: 1rem;
  }

  article li {
    margin-top: 0.5rem;
    margin-bottom: 0.5rem;
  }

  article p {
    margin-top: 1rem;
    margin-bottom: 1rem;
  }
}

Then removed the prose class from my template. The div with dangerouslySetInnerHTML lives inside an <article> element in my page layout, so the scoped styles apply automatically:

<div
  className="mt-6 max-w-none"
  dangerouslySetInnerHTML={{ __html: post.content }}
/>

That's it. Ten lines of CSS. No plugin. No specificity wars. No overriding default styles I didn't want in the first place.

What I'd Tell Past-Me

Looking back at this whole mess, here's what I wish I'd understood from the start:

The typography plugin is genuinely excellent. It's just solving a different problem than the one I had. It assumes you're starting fresh. Zero existing styles, want nice-looking prose out of the box. That wasn't me. I already had a design system, I just needed lists to look like lists.

I also underestimated how different Tailwind v4's plugin architecture is. The old tricks for overriding plugin styles (CSS custom properties, selector matching, even !important) didn't work the way they used to. I was fighting against a framework behavior I didn't understand yet.

The real mistake was assuming the "official" solution was the right solution. It solved the wrong problem in the most complicated way possible.

When the Plugin Would Have Made Sense

I'm not saying the typography plugin is bad. I probably would have reached for it if I was building a new blog without any existing styles. It does a great job of making markdown look decent with minimal effort.

The times I could see myself using it:

  • Before I'd spent time on custom styling (just wanted something that worked)
  • When I needed to prototype quickly and iterate on content first
  • When styling user-generated markdown where I couldn't predict the HTML structure

But once you've got brand colors, custom code styling, and specific link behaviors? You're already fighting the plugin's defaults. The "simple" solution stops being simple.


Have you had plugin conflicts mess up your carefully crafted styles? I'd love to hear your CSS specificity war stories. What plugins have caused you the most headaches?