EmpireUI
Get Pro
← Blog8 min read#tailwind#typography#plugin

@tailwindcss/typography Plugin: Beautiful Prose Styles for Blogs

The @tailwindcss/typography plugin gives you production-ready prose styles out of the box. Here's how to set it up, customize it, and avoid the common pitfalls.

Open typography book with printed text on a wooden desk

What Is the @tailwindcss/typography Plugin?

Tailwind CSS strips all browser defaults. That's the whole point — you get a blank canvas. But the moment you start rendering markdown or CMS content you don't control, a blank canvas is a problem. Your <h2> looks the same as your <p>. Every list loses its bullets. Blockquotes look like regular text.

The @tailwindcss/typography plugin solves this with a single CSS class: prose. Drop it on a wrapper element and everything inside gets opinionated, readable defaults — heading hierarchy, body line-height, code blocks, blockquote styling, the works. It's been around since Tailwind v2 and it's still the fastest way to make generated HTML look good.

Honestly, you'd spend a weekend writing equivalent CSS from scratch and it still wouldn't be as polished. The plugin ships with five size modifiers (prose-sm, prose-base, prose-lg, prose-xl, prose-2xl) and dark mode variants out of the box. For any blog, docs page, or MDX-powered site, it's the right starting point — not something you should roll yourself.

Worth noting: this is an official Tailwind Labs package. It's not a community addon with uncertain maintenance. As of 2026 the repo is actively kept in sync with the main framework, and it supports both Tailwind v3 and the new v4 config system.

Installation and Basic Setup

The install takes about 45 seconds. Add the package, register the plugin, done. ``bash npm install -D @tailwindcss/typography ` Then register it in your tailwind.config.js: `js // tailwind.config.js module.exports = { plugins: [ require('@tailwindcss/typography'), ], } ` If you're on Tailwind v4 with the CSS-first config, add it in your main CSS file instead: `css @import 'tailwindcss'; @plugin '@tailwindcss/typography'; ``

Now apply the prose class to any container wrapping your content: ``jsx export default function BlogPost({ content }) { return ( <article className="prose lg:prose-xl mx-auto max-w-3xl px-4"> <div dangerouslySetInnerHTML={{ __html: content }} /> </article> ); } ` That lg:prose-xl modifier bumps the font size up on wider viewports — a pattern you'll use constantly. The max-w-3xl` caps line length around 65 characters, which is the sweet spot for reading comfort.

Quick aside: the prose class applies max-width to the content wrapper internally via the ch unit, but setting your own max-w-* on the outer element is still worth doing for layout control. Don't assume the plugin's internal max-width is enough — it's not always what you want on constrained layouts.

One more thing — if you're using MDX in a Next.js app, you're probably wrapping your content component in a layout anyway. Put prose on that layout wrapper and you never have to think about it again per-page.

Customizing Prose Styles with Tailwind Theme Extension

The defaults are good. But you'll almost certainly need to tweak them — matching your brand's font, adjusting heading colors, changing link behavior. The plugin exposes the full configuration via theme.extend.typography. ``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]'), '--tw-prose-code': theme('colors.pink[600]'), maxWidth: '72ch', lineHeight: '1.75', 'h2': { fontSize: '1.5rem', fontWeight: '700', marginTop: '2rem', }, }, }, }), }, }, plugins: [require('@tailwindcss/typography')], }; ``

The CSS variables approach (--tw-prose-body, --tw-prose-links, etc.) is the cleaner path for color customization — it plays nicely with dark mode. The direct CSS object approach ('h2': { fontSize: '1.5rem' }) is better for structural changes like spacing and font sizing.

Dark mode support is baked in via prose-invert. Add it alongside a dark-mode class and the plugin swaps to a pre-configured dark palette automatically: ``jsx <article className="prose prose-slate dark:prose-invert lg:prose-lg"> {/* your content */} </article> ` In practice, prose-invert alone is rarely enough — you'll want to override --tw-prose-invert-links` at minimum because the default link color in dark mode is just white, which is boring and low-contrast against anything other than a pure black background.

That said, it's significantly easier to build on prose-invert than to write dark mode prose styles from scratch. Start with it and override the 2-3 variables that don't match your design system.

Overriding Specific Elements with Modifier Classes

The plugin ships with per-element modifier classes you can compose directly in your markup — no config file required. Things like prose-headings:font-bold, prose-a:text-violet-500, prose-code:text-sm. This is the approach you want when you're building a component-level override rather than a site-wide style change. ``jsx <article className={[ 'prose', 'prose-headings:scroll-mt-20', 'prose-headings:font-extrabold', 'prose-a:text-violet-500 prose-a:no-underline hover:prose-a:underline', 'prose-img:rounded-xl prose-img:shadow-md', 'prose-pre:bg-slate-900 prose-pre:text-slate-100', 'lg:prose-lg', ].join(' ')} > {children} </article> ``

The prose-headings:scroll-mt-20 trick is one you'll appreciate the first time someone clicks a table-of-contents anchor and the heading hides behind a sticky nav. Set scroll-mt to whatever your fixed header height is — 80px maps to scroll-mt-20 in Tailwind's default scale.

One gotcha: the element modifiers don't always beat specificity from your base CSS. If you're fighting a custom reset that targets h1 directly, you may need to add !important variants or scope your prose container more tightly. It's rare, but it comes up in older codebases where someone's added a global styles.css alongside Tailwind.

Look, the composability here is the real win. You're not locked into a theme file edit every time you want a one-off style. That flexibility is why @tailwindcss/typography pairs so well with component-based architectures — each layout or page template can have its own prose personality without polluting global config.

Using Prose with MDX and Next.js

MDX in Next.js App Router is where this plugin earns its keep. Your .mdx files render as React components, and you can pass your prose wrapper as the layout: ``tsx // app/blog/[slug]/layout.tsx export default function BlogLayout({ children }: { children: React.ReactNode }) { return ( <main className="min-h-screen bg-white dark:bg-slate-950 py-16"> <article className="prose prose-slate dark:prose-invert lg:prose-xl mx-auto max-w-2xl px-6"> {children} </article> </main> ); } ``

This means every .mdx file under app/blog/ automatically gets prose styles — no className needed in the file itself. The file stays clean, authors don't have to think about CSS, and you can update the layout globally without touching each post.

Where it gets interesting is custom MDX components. You can replace default HTML elements with your own React components via the components prop on MDXRemote or the useMDXComponents hook. For example, replacing <code> with a syntax-highlighted version: ``tsx import { MDXRemote } from 'next-mdx-remote/rsc'; import { CodeBlock } from '@/components/CodeBlock'; const components = { code: ({ className, children, ...props }) => { const language = className?.replace('language-', '') ?? 'text'; return <CodeBlock language={language}>{children}</CodeBlock>; }, }; export default async function BlogPost({ params }) { const { source } = await getPost(params.slug); return <MDXRemote source={source} components={components} />; } ``

Custom components still inherit prose styles for anything you don't override. So your replaced <code> gets the prose-code scoped styles unless your custom component explicitly resets them. It's a bit of a dance at first, but once you've set it up once you'll copy-paste the pattern everywhere. If you're looking for design inspiration beyond basic prose, the glassmorphism components on Empire UI are worth a look for building card layouts around your blog content.

Prose Size Modifiers and Responsive Typography

The five size modifiers aren't just font-size changes — each preset adjusts spacing, line-height, and element proportions as a coherent system. prose-sm targets 14px base, prose-lg targets 18px, prose-xl targets 20px, prose-2xl targets 24px. That last one is the marketing-landing-page scale, not a blog scale.

For a typical developer blog, prose (16px base) on mobile and prose-lg on desktop is the combination that reads best. The jump from 16 to 18px sounds small but the difference in perceived spaciousness is real — especially in dense technical content with lots of code blocks. ``jsx <article className="prose prose-slate md:prose-lg xl:prose-xl dark:prose-invert"> {children} </article> ``

Can you set custom font sizes inside the prose scope without touching the modifier presets? Yes. Target the CSS variable --tw-prose-body along with your own text-* override on the wrapper. But be careful — overriding font size on the container element can break the em-based internal spacing that the plugin uses. The plugin builds everything relative to the body font size, so if you go off-script with a text-[17px] override, check that your heading sizes and margins still look proportional.

Worth noting: if you're using Tailwind v4, the size modifiers are still available but configured slightly differently in the CSS-first setup. Check the tailwind v4 features overview for the config syntax changes — the plugin API itself hasn't changed but the registration method has.

One more thing — the max-w-prose utility class (72ch) from core Tailwind is not the same as the plugin's internal max-width. You can use either or both, but don't be confused when your max-w-prose on the outer div behaves differently from the plugin's own width constraints. Use whichever gives you the layout control you actually need.

What the Plugin Won't Do (And What to Use Instead)

The typography plugin handles one thing: making raw HTML content readable. It doesn't handle syntax highlighting, line numbers in code blocks, copy-to-clipboard buttons, callout boxes, or anything interactive. Those are your problem.

For syntax highlighting, Shiki is the 2025-2026 default for Next.js App Router — it runs server-side and outputs pre-highlighted HTML that prose-pre styles will pick up automatically. Prism still works but requires client-side JS unless you're doing SSG. Pick based on your rendering strategy, not habit.

The plugin also doesn't touch anything outside the prose scope. That's a deliberate decision. Your nav, footer, buttons, and card components are unaffected — no specificity leakage. That containment is part of why it pairs so well with component systems. If you're building a UI around your blog that uses components from Empire UI's box shadow generator or the gradient generator for card accents, none of those get touched by prose styles.

Accessibility is where you still need to pay attention. The plugin won't add aria- attributes, fix heading hierarchy issues in your markdown, or handle focus management for anchor links. Those are authoring problems — prose just styles what you give it. If your markdown source has h1h3 jumps with no h2, the plugin will faithfully render an inaccessible document that looks pretty.

FAQ

Does @tailwindcss/typography work with Tailwind v4?

Yes. In v4's CSS-first config, use @plugin '@tailwindcss/typography'; in your main CSS file instead of the plugins array in tailwind.config.js. The prose class and all modifiers work the same.

Can I use prose styles without a wrapper element?

Not really — prose is a scope class, it needs to wrap the content. You can put it on any block element: article, div, section. Just don't apply it to body or you'll affect everything on the page.

How do I disable prose styles on specific child elements?

Use the not-prose class on any child element you want to opt out. It resets all inherited prose styles on that element and its children, so your custom components stay unaffected inside a prose container.

Why are my code blocks unstyled even with the prose class?

The plugin styles pre > code, not inline code wrapped in other elements. If your syntax highlighter wraps output in a div instead of a pre, the plugin won't target it. Check your highlighter's output structure and adjust the plugin config's pre selector if needed.

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

Read next

Blog Layout in Tailwind: Article Page, Card Grid, SidebarTailwind Typography: @tailwindcss/typography Deep DivePrint Stylesheets in 2026: @media print Best PracticesTailwind vs CSS Modules in 2026: Utility-First vs Scoped Styles