EthicsComputerCourses
Join training +91 90532 10052

Plugins: Typography, Forms & Your Own

Tailwind CSS · Lesson 24 of 38 Intermediate 8 min read Updated

A plugin is a piece of JavaScript that adds utilities, variants or base styles to Tailwind. Most projects need only two: the official typography plugin for articles and the official forms plugin for form controls. Some teams also write their own plugin to share company utilities between several apps.

In this lesson you will go deeper into both official plugins than the short mentions in Typography and Styling Forms, pass options to a plugin from CSS, see when a plugin is the wrong tool, and write a small plugin with the plugin API.

Loading a plugin in v4

Install the package, then name it in your main CSS file with @plugin:

Terminal
npm install -D @tailwindcss/typography @tailwindcss/forms
CSS
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms" {
  strategy: "class";
}

The block after the forms plugin passes options, written like CSS declarations. Each plugin documents its own options.

Some plugins from older tutorials are no longer needed at all. Line clamping (line-clamp-3), aspect ratios (aspect-video) and container queries (@container, @md:) are built into Tailwind 4, so remove those old plugin packages when you see them.

The typography plugin in depth

The plugin adds a prose class that styles every heading, paragraph, list, quote, table and code block inside it. You wrap content you cannot add classes to, such as Markdown from a blog or HTML from a CMS:

HTML
<article class="prose prose-slate lg:prose-lg dark:prose-invert prose-a:text-sky-700 prose-a:hover:text-sky-900 prose-img:rounded-xl">
  <!-- HTML from the CMS goes here -->
</article>

Each class has a job:

ClassWhat it does
proseThe base styles; always required
prose-slateUses the slate greys (also prose-gray, the default, prose-zinc, prose-neutral, prose-stone)
prose-sm to prose-2xlChanges the whole type scale; works with breakpoints, as in lg:prose-lg
dark:prose-invertSwitches to the light-on-dark colour set
prose-a:, prose-h2:, prose-img: ...Element modifiers: apply a utility to one kind of element inside
max-w-noneRemoves the built-in maximum width so the article fills its column
not-prosePut on a wrapper inside the article to switch prose styles off there

Element modifiers exist for headings, paragraphs, links, lists, tables, images, code and more. When you add a state, put it after the element modifier: prose-a:hover:text-sky-900. (In v3 the order was reversed.) Here is the real CSS the CLI generated for prose-a:text-sky-700:

Output
.prose-a\:text-sky-700 :is(:where(a):not(:where([class~="not-prose"], [class~="not-prose"] *))) {
  color: var(--color-sky-700);
}

The selector targets links inside the article but skips anything inside a not-prose wrapper, which is how an embedded widget, such as a "Book a test drive" form, keeps its own look.

A brand colour theme for prose

The prose colours come from CSS variables named --tw-prose-body, --tw-prose-headings, --tw-prose-links and so on, with --tw-prose-invert-* versions for dark mode. To make your own theme, define a utility that sets them:

CSS
@utility prose-saffron {
  --tw-prose-body: var(--color-stone-800);
  --tw-prose-headings: var(--color-orange-900);
  --tw-prose-links: var(--color-orange-700);
  --tw-prose-bullets: var(--color-orange-400);
  --tw-prose-quote-borders: var(--color-orange-300);
}

Then write class="prose prose-saffron". If prose clashes with a class your CMS already uses, the plugin accepts className: wysiwyg; as an option, and every class becomes wysiwyg, wysiwyg-lg, not-wysiwyg and so on.

Without the plugin

On a quick prototype, or anywhere the plugin is not installed, you can style simple content with arbitrary variants. [&_h2]: means "every h2 inside me". This works for a few elements, but the plugin is far better for full articles because it also handles spacing between every pair of elements.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Content styles without a plugin</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="p-6">
  <article class="max-w-prose text-slate-700 [&_a]:font-medium [&_a]:text-sky-700 [&_a]:underline [&_h2]:mt-6 [&_h2]:text-xl [&_h2]:font-bold [&_h2]:text-slate-900 [&_li]:ms-5 [&_p]:mt-3 [&_ul]:mt-3 [&_ul]:list-disc">
    <h2>Monsoon cycling in Pune</h2>
    <p>Roads near Sinhagad get slippery after the first rains. Check your <a href="#">brake pads</a> before July.</p>
    <ul>
      <li>Carry a rain jacket</li>
      <li>Fit mudguards</li>
      <li>Ride slower on painted lines</li>
    </ul>
    <h2>Route of the week</h2>
    <p>Khadakwasla dam loop, 32 km, mostly flat.</p>
  </article>
</body>
</html>

The forms plugin in depth

By default the forms plugin restyles every text input, select, textarea, checkbox and radio on the page (its base strategy). That suits a new project. In an existing project, where some forms already have their own look, global changes can break pages. The strategy option fixes this:

  • strategy: "base" styles elements globally and creates no classes.
  • strategy: "class" styles nothing globally; you opt in with classes such as form-input, form-select, form-textarea, form-checkbox and form-radio.

With the plugin, a checkbox takes its colour from the text colour, so class="form-checkbox rounded text-emerald-600" gives a green tick.

Writing your own plugin

Before writing JavaScript, ask whether @utility and @custom-variant can do it. They usually can, and they need no build step to understand. This price utility puts a rupee sign before an amount, entirely in CSS:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>@utility before a plugin</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
  <style type="text/tailwindcss">
    @utility price {
      font-variant-numeric: tabular-nums;
      &::before {
        content: "₹";
        margin-inline-end: 0.125em;
      }
    }
  </style>
</head>
<body class="p-6">
  <ul class="max-w-xs divide-y divide-slate-200 rounded-xl border border-slate-200">
    <li class="flex justify-between p-3">Masala dosa <span class="price font-semibold">90</span></li>
    <li class="flex justify-between p-3">Filter coffee <span class="price font-semibold">35</span></li>
    <li class="flex justify-between p-3">Family thali <span class="price font-semibold">1,299</span></li>
  </ul>
</body>
</html>

A JavaScript plugin earns its place when you need options, want to loop over theme values in code, or want to publish the same utilities as an npm package for several apps. Here is a plugin that makes the currency symbol an option, adds a glow-* utility for every theme colour, and adds a festive variant:

JavaScript
// rupee-plugin.mjs
import plugin from "tailwindcss/plugin";

export default plugin.withOptions((options = {}) => {
  const symbol = options.symbol ?? "₹";

  return ({ addUtilities, matchUtilities, addVariant, theme }) => {
    addUtilities({
      ".price": {
        "font-variant-numeric": "tabular-nums",
        "&::before": { content: `"${symbol}"`, "margin-inline-end": "0.125em" },
      },
    });

    matchUtilities(
      { glow: (value) => ({ "box-shadow": `0 0 0 4px ${value}` }) },
      { values: theme("colors"), type: "color" },
    );

    addVariant("festive", "&:where(.festive, .festive *)");
  };
});

Load it with a relative path and an option, @plugin "./rupee-plugin.mjs" { symbol: "Rs."; }. With the classes price, glow-amber-300 and festive:glow-rose-300 in the templates, the real CLI produced:

Output
.price {
  font-variant-numeric: tabular-nums;
  &::before {
    content: "Rs.";
    margin-inline-end: 0.125em;
  }
}
.glow-amber-300 {
  box-shadow: 0 0 0 4px oklch(87.9% 0.169 91.605);
}
.festive\:glow-rose-300:where(.festive, .festive *) {
  box-shadow: 0 0 0 4px oklch(81% 0.117 11.638);
}

addUtilities adds fixed classes, matchUtilities adds a family of classes from a set of values (here every colour in the theme), and addVariant registers a variant. There are also addBase for global element styles and addComponents for component classes. plugin.withOptions is only needed when the plugin takes options; a plain plugin(({ addUtilities }) => { ... }) works otherwise.

Common mistakes

  • Expecting plugins in the browser build. They need a real build with the CLI, Vite or PostCSS.
  • Forgetting the base class. prose-lg or prose-slate alone do nothing; always add prose too.
  • Wrong modifier order. In v4 write prose-a:hover:underline, not hover:prose-a:underline.
  • Turning on the forms plugin in an old project without testing. Use strategy: "class" to avoid surprises.
  • Writing a JavaScript plugin for one simple class. @utility is shorter and easier for the team to read.

Key points

  • Load plugins in CSS with @plugin "package-name"; and pass options in a block after it.
  • The typography plugin's prose styles CMS and Markdown content, with size, grey scale, dark:prose-invert and element modifiers such as prose-a:.
  • not-prose switches prose styles off inside an article; custom colour themes set the --tw-prose-* variables.
  • The forms plugin's strategy option chooses global styles (base) or opt-in classes such as form-input (class).
  • Write a JavaScript plugin with tailwindcss/plugin only when you need options, loops over the theme or a shared package.
  • The browser build cannot load plugins or config files.

Exercise

A news site in Lucknow shows articles from a CMS. Write the CSS and the HTML wrapper so that: articles use the typography plugin with the stone greys; text is larger from the lg breakpoint; links are rose-700 and underline on hover; dark mode uses the inverted colours; and an embedded "subscribe" box inside the article keeps its own styles. Also load the forms plugin so it only styles elements that have form-* classes.

Show solution
CSS
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms" {
  strategy: "class";
}
HTML
<article class="prose prose-stone lg:prose-lg dark:prose-invert prose-a:text-rose-700 prose-a:hover:underline">
  <!-- CMS HTML -->
  <div class="not-prose rounded-xl bg-rose-50 p-4">
    <label for="sub-email" class="block text-sm font-medium">Daily headlines by email</label>
    <input id="sub-email" type="email" class="form-input mt-1 w-full rounded-lg">
  </div>
</article>

not-prose protects the subscribe box from article styles, and with the class strategy only the input with form-input gets the plugin's reset.

Quick Quiz

1. How do you load the typography plugin in Tailwind 4?

2. Which class stops prose styles inside part of an article?

3. What does the forms plugin's strategy: "class" option do?

4. When is a JavaScript plugin a better choice than @utility?

Learn Tailwind CSS with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call