Type something to search...
How to Use the Tailwind Typography Plugin for Markdown Content?

How to Use the Tailwind Typography Plugin for Markdown Content?

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:

PropertyDefault prose value
Body font size1rem (16px)
Body line height1.75
Max width65ch
Paragraph spacing1.25em top and bottom
h1 size2.25em
h2 size1.5em
Link styleUnderlined, weight 500
Inline codeWeight 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:

ClassBody sizeLine height
prose-sm0.875rem1.714
prose-base1rem1.75
prose-lg1.125rem1.778
prose-xl1.25rem1.8
prose-2xl1.5rem1.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-h1 to prose-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

  1. Applying prose to the whole layout. Navigation, footers, and forms inherit content styling and break. Scope it to the article body.
  2. Forgetting remark-gfm. Tables and task lists render as plain text, and people assume the plugin is broken.
  3. Using max-w-none without another constraint. You lose the readable line length that made the defaults good.
  4. Overriding with high-specificity selectors. .prose h2 beats modifiers and makes later tweaks painful. Use :where().
  5. 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:

  1. GitHub: tailwindlabs/tailwindcss-typography — the official README with every modifier and customization option.
  2. Tailwind CSS Docs: Functions and directives — how @plugin, @config, and @custom-variant work in v4.
  3. Next.js Docs: MDX — rendering Markdown and MDX in the App Router.
  4. W3C: Understanding WCAG 1.4.12 Text Spacing — the spacing overrides your content styles must survive.
Share :

Related Posts

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

You align an icon next to a button label and it looks a pixel or two too high, no matter how you adjust vertical-align. You set overflow: hidden

Continue Reading
Are Google Fonts GDPR-Compliant?

Are Google Fonts GDPR-Compliant?

In late 2022, thousands of small business owners in Germany and Austria opened letters demanding a few hundred euros in "damages" because their websi

Continue Reading
How to Audit Web Font Performance with Lighthouse?

How to Audit Web Font Performance with Lighthouse?

A client sends you a screenshot of their PageSpeed Insights report: performance score 61, LCP 3.9 seconds, and a vague list of warnings. They want to

Continue Reading