Tailwind Typography: @tailwindcss/typography Deep Dive
Master @tailwindcss/typography: configure prose classes, override defaults, and style rich content without fighting the CSS cascade.
What @tailwindcss/typography Actually Does
If you've ever dropped raw HTML from a CMS into a Tailwind project and watched all your headings render at the same size as body text, you already know the problem. Tailwind resets everything. That's by design — but it means unstyled content looks terrible until you do the work yourself.
@tailwindcss/typography ships a single utility class, prose, that applies an opinionated but well-considered set of typographic defaults to any HTML content inside it. Released alongside Tailwind CSS v2 and currently sitting at v0.5.x as of 2026, it handles h1 through h6, p, ul, ol, blockquote, code, pre, table, and more — all in one shot.
Honestly, it's one of the most useful official plugins Tailwind has ever shipped. You're not reinventing typographic scale every time you build a blog or docs site. Just wrap your content, move on.
Installation and Basic Setup
Getting it wired up takes about two minutes. Install the package, add it to your config, and you're done.
npm install @tailwindcss/typographyThen in your tailwind.config.js (or .ts if you're on v4's config approach):
``js
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
plugins: [
require('@tailwindcss/typography'),
],
};
``
Now add prose to any wrapper element and the plugin styles every descendant HTML element automatically:
``jsx
export default function BlogPost({ content }) {
return (
<article className="prose prose-lg mx-auto max-w-3xl px-4">
<div dangerouslySetInnerHTML={{ __html: content }} />
</article>
);
}
``
That prose-lg modifier bumps all the base sizes up. There's also prose-sm, prose-xl, and prose-2xl. Worth noting: prose-lg sets body text at 18px (1.125rem), which tends to read better for long-form content than the default 16px base.
Prose Modifiers and Dark Mode
The plugin ships with colour scheme variants. prose-gray is the default, but you've also got prose-slate, prose-zinc, prose-neutral, prose-stone, and even accent colours like prose-indigo or prose-sky that tint links and headings.
Dark mode is where it clicks. Add prose-invert under your dark mode selector and the entire typographic palette flips to light-on-dark automatically. No manual overrides required for the basics:
``jsx
<article className="prose prose-lg dark:prose-invert">
{/* your content */}
</article>
``
That said, prose-invert doesn't always produce exactly the contrast ratios you want. If you're building something where accessibility is non-negotiable — say, a developer docs site — you'll want to validate the output against WCAG 2.1 AA. The defaults are close, but not always perfect on mid-range dark backgrounds like gray-800.
Quick aside: if you're building UI components that sit next to this styled content, it's worth keeping a consistent design language across both. The glassmorphism components on Empire UI, for example, pair well with prose-slate if you're going for that frosted-glass + editorial aesthetic.
Customising Typography with the theme() Config
Out of the box defaults are fine. But you'll almost always need to tweak something — maybe your brand uses a different heading weight, or you want 32px gap above H2s instead of the default 48px.
The plugin exposes a typography key inside your theme config. Every element, every size variant, every CSS property is overridable:
``js
// tailwind.config.js
module.exports = {
theme: {
extend: {
typography: ({ theme }) => ({
DEFAULT: {
css: {
'--tw-prose-body': theme('colors.slate[700]'),
'--tw-prose-headings': theme('colors.slate[900]'),
'--tw-prose-links': theme('colors.violet[600]'),
h2: {
fontWeight: '700',
marginTop: '2rem',
},
'code::before': { content: '""' },
'code::after': { content: '""' },
},
},
}),
},
},
plugins: [require('@tailwindcss/typography')],
};
``
That code::before and code::after trick removes the default backtick wrapping around inline code. It's one of the first things you'll want to nuke if you're applying your own code styling. In practice, most teams do this on day one and never look back.
One more thing — CSS custom properties (the --tw-prose-* variables) are the cleaner path for colour overrides compared to nesting under css. Use them when you can; they play nicer with dark mode toggling at runtime.
Using prose with MDX and Next.js
Most real-world use of this plugin happens inside MDX pipelines. Whether you're on Next.js 14 App Router, Astro, or Remix, the pattern is the same: compile MDX to HTML, then wrap with prose.
In the Next.js App Router with @next/mdx, you'd typically create a layout component:
``tsx
// app/blog/[slug]/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<main className="min-h-screen bg-white dark:bg-gray-950 py-16">
<article className="prose prose-lg prose-slate dark:prose-invert mx-auto max-w-2xl px-6">
{children}
</article>
</main>
);
}
``
This layout wraps every blog post automatically. You don't touch individual MDX files. The plugin handles the rest. How much time does that save across a 50-post blog? A lot.
One thing to watch: if you're mixing Tailwind utility classes inside MDX files (for custom callout boxes, for example), you'll sometimes hit specificity clashes with prose styles. The fix is usually adding not-prose to the element you want to opt out of prose styling — a built-in escape hatch the plugin provides exactly for this.
Overriding Specific Elements Without Breaking the Rest
The not-prose class is your best friend for inlining custom components inside prose content. Say you want a styled callout box that doesn't inherit the prose margins and colours:
``jsx
<div className="not-prose my-8 rounded-xl border border-violet-200 bg-violet-50 p-6">
<p className="text-sm font-semibold text-violet-700">Note</p>
<p className="mt-1 text-sm text-violet-600">This sits outside the prose cascade entirely.</p>
</div>
``
Look, this matters more than people realise. Without not-prose, the prose plugin will try to style paragraphs inside that div too — and you'll spend 20 minutes wondering why your custom colours aren't sticking.
For fine-tuning specific spacing, the plugin uses em-based margins internally rather than rem. That means all spacing scales proportionally with the prose-sm / prose-lg size modifiers. Useful to know when you're trying to match the vertical rhythm of a design system that also works in em units.
If you're building a component library or design system alongside your content layer, take a look at Empire UI's component collection. You'll find plenty of UI primitives that are built to sit cleanly next to prose-styled content without fighting it.
Performance and When to Skip the Plugin
The plugin generates a non-trivial number of CSS rules. In production with PurgeCSS / Tailwind's content scanning, unused variants get stripped, so the final bundle impact is minimal. But if you're checking your CSS bundle in development, don't panic at the size.
That said, if your project only has one or two static pages with manually authored HTML, you might not need the plugin at all. Writing 15 lines of custom CSS for a single blog post is perfectly reasonable. The plugin earns its keep when you're styling content you don't control — CMS output, MDX, third-party markdown.
Worth noting: the plugin doesn't touch anything outside a .prose wrapper. That's intentional and it's what makes it safe to add to any existing project. There's zero risk of it touching your UI components, nav, or footer styles. It's strictly opt-in.
For projects where typography is just one piece of a larger visual system — say, a docs site that also needs UI components, illustrations, and custom cursors — treating prose as one isolated layer while building the rest with standard Tailwind utilities is the cleanest approach. They don't step on each other.
FAQ
Yes, v0.5.x of the plugin is compatible with Tailwind v4. You'll configure it as a plugin in your CSS file rather than tailwind.config.js if you've fully migrated to v4's CSS-first config.
Add not-prose to the element or its wrapper. Everything inside not-prose is excluded from the prose cascade entirely — useful for custom callouts, embeds, or UI components dropped into MDX.
No — they conflict. Pick one: prose-sm, prose (default), prose-lg, prose-xl, or prose-2xl. Use responsive prefixes like prose-sm md:prose-lg if you need size changes at breakpoints.
Use the --tw-prose-links CSS variable in your theme config rather than targeting a directly. The plugin uses these variables internally, so variable overrides win without specificity fights.