EmpireUI
Get Pro
← Blog8 min read#tailwind typography#prose#blog

Tailwind Typography: @tailwindcss/typography Deep Dive

Master @tailwindcss/typography: configure prose classes, override defaults, and style rich content without fighting the CSS cascade.

Developer writing code on laptop with typography and text styling on screen

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/typography

Then 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

Does @tailwindcss/typography work with Tailwind CSS v4?

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.

How do I stop prose from styling a specific child element?

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.

Can I use multiple prose size modifiers on the same element?

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.

Why isn't my custom link colour overriding the prose default?

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.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

@tailwindcss/typography Plugin: Beautiful Prose Styles for BlogsBlog Layout in Tailwind: Article Page, Card Grid, SidebarGlassmorphism Blog Layout: Frosted Article Cards and Reading ViewVirtualizing Long Lists in React: TanStack Virtual Deep Dive