
How to Use the Tailwind Typography Plugin for Markdown Content?
- Sajjad
- Typography
- 01 Oct, 2026
You render a Markdown blog post into a Tailwind project and every heading looks like body text, the bullet points are gone, links are indistinguishable from the paragraphs around them, and code blocks run straight into the next line. Nothing is broken. Tailwind's preflight reset deliberately strips default browser styles from every element, which is great for components and terrible for content you do not control the markup of. You cannot add utility classes to HTML generated from Markdown, a CMS, or an MDX renderer, so you need a different approach.
That approach is the official @tailwindcss/typography plugin. This article covers how to install it in Tailwind v4, how the prose classes work, how to size, color, and dark-mode your content, how to customize individual elements, and how to integrate it with an MDX pipeline in Next.js.
What the Typography Plugin Does
The plugin adds a prose class. Put it on a wrapper element and every descendant element that comes from Markdown, such as headings, paragraphs, lists, blockquotes, tables, code, images, and horizontal rules, gets a coordinated set of typographic styles: a type scale, vertical spacing, list markers, link underlines, and table borders.
The defaults are designed for long-form reading. Out of the box, prose gives you:
| Property | Default prose value |
|---|---|
| Body font size | 1rem (16px) |
| Body line height | 1.75 |
| Max width | 65ch |
| Paragraph spacing | 1.25em top and bottom |
h1 size | 2.25em |
h2 size | 1.5em |
| Link style | Underlined, weight 500 |
| Inline code | Weight 600, backtick decoration |
The 65ch width keeps lines inside the 45–75 character range that is comfortable to read, which is one of the reasons the defaults work well without tuning. The post on ideal line length for web content explains why.
Installing the Plugin in Tailwind v4
Install the package as a dev dependency:
npm install -D @tailwindcss/typography
Tailwind v4 is configured in CSS, so you register the plugin with the @plugin directive in your main stylesheet instead of a plugins array:
/* src/styles/main.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
That is all the setup required. If you are still on a JavaScript config loaded through @config, the old require("@tailwindcss/typography") entry in plugins continues to work, but the CSS form is the v4 default. For the rest of your type system, such as font families and the base scale, see how to configure typography in Tailwind CSS v4.
Applying prose to Markdown and MDX
Wrap the rendered content, not the whole page:
<article class="prose">
<h1>Release notes</h1>
<p>This release adds...</p>
<ul>
<li>Faster builds</li>
<li>New API</li>
</ul>
</article>
In a Next.js App Router project rendering MDX with next-mdx-remote, it looks like this:
// src/app/blog/[single]/page.tsx (simplified)
import { MDXRemote } from "next-mdx-remote/rsc";
import remarkGfm from "remark-gfm";
import { getPost } from "@/lib/posts";
export default async function Post({
params,
}: {
params: Promise<{ single: string }>;
}) {
const { single } = await params;
const post = await getPost(single);
return (
<article className="prose prose-lg mx-auto dark:prose-invert">
<h1>{post.title}</h1>
<MDXRemote
source={post.content}
options={{ mdxOptions: { remarkPlugins: [remarkGfm] } }}
/>
</article>
);
}
remark-gfm matters here. Without it, Markdown tables, strikethrough, and task lists are not converted to HTML, so the plugin's table styles have nothing to apply to.
Removing the Max Width
The 65ch limit is right for most articles, but if your layout already constrains the column, it can fight your grid. Add max-w-none:
<div class="prose max-w-none">...</div>
Only do this if the parent keeps line length in check. A full-width prose block on a 1440px screen produces lines well over 100 characters.
Sizing with prose Modifiers
Size modifiers scale the whole system proportionally, including headings, spacing, and code blocks, because most prose values are defined in em:
| Class | Body size | Line height |
|---|---|---|
prose-sm | 0.875rem | 1.714 |
prose-base | 1rem | 1.75 |
prose-lg | 1.125rem | 1.778 |
prose-xl | 1.25rem | 1.8 |
prose-2xl | 1.5rem | 1.667 |
Combine them with responsive variants so content gets slightly larger on wide screens:
<article class="prose md:prose-lg lg:prose-xl">...</article>
Avoid prose-sm for primary reading content. 14px body text is too small for most readers on most screens. Reserve it for sidebars, comments, or card excerpts.
Gray Scales and Dark Mode
The plugin ships with five gray themes that match Tailwind's palettes: prose-gray (the default), prose-slate, prose-zinc, prose-neutral, and prose-stone. Pick the one that matches the rest of your UI:
<article class="prose prose-slate">...</article>
For dark mode, add prose-invert under your dark variant. It swaps every color variable to a dark-background counterpart:
<article class="prose prose-slate dark:prose-invert">...</article>
If your site uses class-based dark mode, make sure the dark variant is configured accordingly in v4:
@custom-variant dark (&:where(.dark, .dark *));
Check the result against WCAG 1.4.3, which requires 4.5:1 contrast for normal text and 3:1 for large text. The default inverted grays pass for body copy, but if you customize colors, re-test them. The WCAG contrast requirements guide lists the thresholds in detail.
Customizing Individual Elements with Modifiers
Element modifiers let you style one kind of element inside prose straight from your markup. The pattern is prose-{element}:{utility}:
<article
class="prose prose-slate
prose-headings:font-display prose-headings:tracking-tight
prose-a:text-blue-700 prose-a:underline-offset-4 hover:prose-a:text-blue-900
prose-img:rounded-xl
prose-code:before:content-none prose-code:after:content-none"
>
...
</article>
The available element modifiers include:
- Text:
prose-headings,prose-h1toprose-h4,prose-p,prose-lead,prose-strong,prose-em - Links and quotes:
prose-a,prose-blockquote - Lists:
prose-ul,prose-ol,prose-li - Code:
prose-code,prose-pre,prose-kbd - Tables:
prose-table,prose-thead,prose-tr,prose-th,prose-td - Media:
prose-img,prose-video,prose-figure,prose-figcaption,prose-hr
The prose-code:before:content-none pair is the most common tweak. By default the plugin wraps inline code in literal backtick characters through pseudo-elements, which many designers do not want once inline code has its own background.
Modifiers are great for a few adjustments. If your class list grows past a couple of lines, move the styling into CSS instead.
Customizing the Theme in CSS
The plugin reads its colors from CSS custom properties on the prose element. Overriding them is the cleanest way to brand the content without fighting specificity:
@layer components {
.prose {
--tw-prose-body: var(--color-slate-700);
--tw-prose-headings: var(--color-slate-950);
--tw-prose-links: var(--color-blue-700);
--tw-prose-bold: var(--color-slate-950);
--tw-prose-quotes: var(--color-slate-800);
--tw-prose-quote-borders: var(--color-blue-300);
--tw-prose-code: var(--color-pink-700);
--tw-prose-pre-bg: var(--color-slate-900);
--tw-prose-pre-code: var(--color-slate-100);
--tw-prose-bullets: var(--color-slate-400);
--tw-prose-hr: var(--color-slate-200);
--tw-prose-th-borders: var(--color-slate-300);
--tw-prose-td-borders: var(--color-slate-200);
}
}
Each variable has an --tw-prose-invert-* twin (for example --tw-prose-invert-links) that prose-invert swaps in, so set those as well if you customize dark mode.
For structural changes, such as heading fonts, blockquote styling, or table spacing, target descendants with :where() to keep specificity low and avoid breaking modifiers:
@layer components {
.prose :where(h1, h2, h3):not(:where(.not-prose, .not-prose *)) {
font-family: var(--font-display);
text-wrap: balance;
}
.prose :where(blockquote):not(:where(.not-prose, .not-prose *)) {
font-style: normal;
border-inline-start-width: 3px;
}
.prose :where(p):not(:where(.not-prose, .not-prose *)) {
text-wrap: pretty;
}
}
The :not(:where(.not-prose, .not-prose *)) guard mirrors what the plugin does internally, so your overrides also respect opt-outs. If you need deeper changes, v4 still accepts a JavaScript typography theme extension through @config, but CSS overrides cover nearly every real-world case.
Renaming the prose Class
If prose conflicts with an existing class, rename it with a plugin option:
@plugin "@tailwindcss/typography" {
className: "wysiwyg";
}
Modifiers follow the new name, so wysiwyg-lg and wysiwyg-a:text-blue-700 become the class names.
Embedding Components with not-prose
MDX lets you drop React components into Markdown, such as callouts, tabs, or pricing cards. Those components have their own Tailwind styling, and prose will happily restyle their headings and lists. Wrap them in not-prose:
// src/layouts/shortcodes/Notice.tsx
export default function Notice({ children }: { children: React.ReactNode }) {
return (
<div className="not-prose my-6 rounded-lg border border-amber-300 bg-amber-50 p-4 text-sm text-amber-900">
{children}
</div>
);
}
Note that you cannot nest prose back inside a not-prose block. If a callout needs Markdown-like styling inside, style it explicitly.
Styling Code Blocks and Tables
The plugin gives code blocks a dark background and monospace font, but it does not do syntax highlighting. Pair it with a highlighter such as Shiki through rehype-pretty-code, and let the highlighter control token colors while prose controls spacing and font size. When a highlighter sets its own background, set --tw-prose-pre-bg to transparent or match it to avoid a double frame. For the typographic side of code, see how to style code snippets.
Wide tables overflow narrow screens. Since you cannot add a wrapper in Markdown, handle it in CSS:
@layer components {
.prose :where(table):not(:where(.not-prose, .not-prose *)) {
display: block;
overflow-x: auto;
font-variant-numeric: tabular-nums;
}
}
display: block with overflow-x: auto lets the table scroll horizontally on its own instead of widening the page. Tabular figures keep numeric columns aligned.
Common Mistakes
- Applying
proseto the whole layout. Navigation, footers, and forms inherit content styling and break. Scope it to the article body. - Forgetting
remark-gfm. Tables and task lists render as plain text, and people assume the plugin is broken. - Using
max-w-nonewithout another constraint. You lose the readable line length that made the defaults good. - Overriding with high-specificity selectors.
.prose h2beats modifiers and makes later tweaks painful. Use:where(). - Not testing user text spacing. Apply the WCAG 1.4.12 overrides (line height 1.5, paragraph spacing 2em, letter spacing 0.12em, word spacing 0.16em) with a bookmarklet and confirm nothing clips.
Tailwind Typography Plugin FAQ
Yes. Install the package and register it in your main stylesheet with the @plugin directive. The prose classes, size modifiers, element modifiers, and dark mode inversion all work the same way as in earlier versions.
Tailwind's preflight reset removes default browser styles from headings, lists, and other elements. Wrap the rendered Markdown in an element with the prose class from the typography plugin to restore readable content styles.
Add the max-w-none class alongside prose. Only do this when a parent container already limits the column width, because the default 65ch limit is what keeps lines at a comfortable reading length.
The plugin adds backtick characters to inline code with before and after pseudo-elements. Add the prose-code:before:content-none and prose-code:after:content-none modifiers, or override the pseudo-elements in your CSS.
Wrap the component in an element with the not-prose class. Everything inside it is excluded from typography plugin styles, so the component keeps its own utility classes.
Yes. Override the tw-prose custom properties, such as the body, headings, and links variables, on the prose class in your CSS. Set the matching invert variables too if you use dark mode.
Conclusion
The Tailwind Typography plugin solves a specific problem well: giving HTML you do not control, like rendered Markdown and MDX, a complete, readable set of typographic styles with a single class. In Tailwind v4, setup is one @plugin line, and from there prose, a size modifier, a gray theme, and dark:prose-invert get most sites to a good default in minutes.
Customize in layers. Use element modifiers for small tweaks, override the --tw-prose-* variables for brand colors, and use low-specificity :where() selectors for structural changes. Keep prose scoped to the content column, wrap embedded components in not-prose, and keep the readable line length unless your layout enforces its own. That keeps your content styling predictable as the site grows.
Here are some useful references for going deeper on the Tailwind Typography plugin:
- GitHub: tailwindlabs/tailwindcss-typography — the official README with every modifier and customization option.
- Tailwind CSS Docs: Functions and directives — how
@plugin,@config, and@custom-variantwork in v4. - Next.js Docs: MDX — rendering Markdown and MDX in the App Router.
- W3C: Understanding WCAG 1.4.12 Text Spacing — the spacing overrides your content styles must survive.


