
Configure Tailwind as a PostCSS plugin to use it in VitePress Markdown, JS/TS, and Vue files

The use of Tailwind I’m talking about
I see Tailwind used in two ways: as a JS API for CSS with significant theme customization and plugin creation, and as an off-the-shelf design system with few or no modifications to the configuration. This article applies to the former.
I’ve used Tailwind a lot, in a lot of different stacks. The agency team I was on in late 2017 learned about Tailwind in its first weeks, was using it on most new projects by May 2018’s v1.0.0, and made it our official standard later than year; I stayed with that team another five years, and we used Tailwind on every project that entire time. Setting Tailwind up on many projects, and seeing my teammates set it up on many others, let my approach to using it evolve quickly. Here’s how I Tailwind, grown out of years using Tailwind to style UIs from large to small, generic to fully custom: what I put where, how I name things, what of the defaults I keep, what I refactor, and what I scrap.
Highlights:
extending.font-light, leading-loose, tracking-widest) I rename them quantitatively (font-300, leading-2, tracking-0.1)p-7) I switch to pixel sizes (p-28)screens’ breakpoints from pixel values to rem valuesDEFAULT key.I prefer to keep as much as possible in the Tailwind config. I’d rather scroll up and down one file, with code folding, than keep things in different places. Custom utilities, components, and variants are defined as plugins directly in the Tailwind config. Arbitrary CSS (for example global styling of specific HTML elements) is defined directly in the Tailwind config.
Two exceptions:
@font-face in addBase has given me trouble in the past, forcing me to put them a @layer base { … } in my CSS entry file (immediately after @tailwind base;).@layer) or, if there are a lot of them, in a separate file.extend or not?My first look is to override (theme.<core plugin>). I extend (theme.extend.<core plugin>) when I know I’ll have use for the Tailwind defaults. This keeps the theme —and, so, the editor intellisense suggestions when I’m in component files— as focused as possible. That typically means, at a minimum, overriding colors, fontFamily, and spacing.
I extend gridTemplateColumns and zIndex if I need custom values.
I’ve sometimes extended aspectRatio, using Tailwind’s “square” and “video”, and I’ve sometimes overridden it.
I use rem sizes a lot, motivated by accessibility. (If that’s new to you, add Josh Comeau’s article on pixels vs rems to your reading list. It aligns well with my own preferred practices, and has good examples.) But I find it easier to think in pixels, and have typically worked from pixel-based designs.
Fluid sizing
For fluid sizing (not counting fluid font sizing) I use my JS utility function css-fluid-length.
So I use a pixels-to-rems function in the Tailwind config. I write pixel values, and the CSS uses rems. This function is handy elsewhere, for example in Font size and line height and Spacing.
const plugin = require('tailwindcss/plugin');
const rootFontSizePx = 16;
/**
* pxToRem
* Given a size in pixels, returns a size rems
*
* @param {Number} px Size to convert to rems, in unitless pixels
* @param {Number} rootPx Size rems are relative to, in unitless pixels
* @returns
*/
function pxToRem(px, rootPx = rootFontSizePx) {
const rem = `${px / rootPx}rem`;
return rem;
}
module.exports = {
theme: {
fontSize: {
24: pxToRem(24),
},
},
plugins: [
plugin(function({ addBase }) {
addBase({
':root': {
fontSize: `${rootFontSizePx}px`,
},
}),
}),
],
}Override colors with the site palette.
Keys are as CSS font-weight values (400 is baseline, 500 is darker/heavier/stronger, etc.). That happens to match Tailwind’s default config.
No DEFAULTs. I prefer to be explicit in the markup: text-blue-400 requires less domain knowledge than text-blue, and can make it easier for new contributors to join the project.
theme: {
colors: {
// global
inherit: 'inherit',
// keyword
current: 'currentColor',
transparent: 'transparent',
// palette
black: '#0a0a0a',
white: '#fff',
red: {
400: /* … */,
},
green: {
300: /* … */,
400: /* … */,
},
blue: {
400: /* … */,
500: /* … */,
},
// …
},
}Unless otherwise specified in the design, I use Tailwind’s default fallback stacks.
const defaultTheme = require("tailwindcss/defaultTheme");
module.exports = {
theme: {
fontFamily: {
sans: ["My Font", ...defaultTheme.fontFamily.sans],
},
},
};Font size values are string rem sizes, written with a “pixels-to-rems” function (see Sizing).
Fluid sizing
For fluid font sizes I use my plugin tailwindcss-fluid-font-size.
If the design system has semantically named text styles (e.g. “Body”), font size keys follow the design system body: …. Otherwise, and for outliers, font size keys are unitless pixel sizes (28: pxToRem(28)). Fluid values’ keys include the breakpoint’s names (see tailwindcss-fluid-font-size).
I’m conservative about taking advantage of Tailwind’s support for multiple properties in font size values. Unless the design very tightly ties styles to each other, I’ll use multiple classes (leading-… text-…). And when I do combine font-size and other properties, I first extend the other properties and then reuse values in font-size:
const lineHeight = {
/* … */
3: "3",
};
module.exports = {
theme: {
fontSize: {
12: pxToRem(12),
24: pxToRem(24),
body: [pxToRem(20), lineHeight["3"]],
},
...lineHeight,
},
};I replace Tailwind’s typographer jargon font weight keys (e.g. “medium”) with CSS’s hundred-increment numbers (“500”).
Keys are identical to the value. Values are strings.
theme: {
fontWeight: {
100: '100',
200: '200',
300: '300',
400: '400',
500: '500',
600: '600',
700: '700',
800: '800',
900: '900',
}
}There are some oddities in Tailwind’s default theme’s height, minWidth, minHeight, maxWidth, and maxHeight.
height and width include utilities for percentage values (e.g. w-1/2), but max-height and min-height do not.width includes utilities for percentage values on a 12-column scale (e.g. w-1/12), but height does not.width includes utilities for 100dvw, 100lvw, 100svw, and 100vw (w-dvw, w-lvw, w-svw, and w-screen respectively), but max-width and min-width do not.height includes utilities for 100dvh, 100lvh, 100svh, and 100vh (h-dvw, h-lvw, h-svw, and h-screen respectively), but max-height and min-height do not.I equalize those all by combining the default theme’s width and height, and applying that to all of width, height, minWidth, minHeight, maxWidth, and maxHeight. (I extend width with defaultTheme.height even though as of this writing width isn’t missing anything height has, as future-proofing.)
const defaultTheme = require('tailwindcss/defaultTheme');
/** @type {import('tailwindcss').Config} */
module.exports {
theme: {
extend: {
height: defaultTheme.width,
maxHeight: ({ theme }) => theme('height'),
minHeight: ({ theme }) => theme('height'),
maxWidth: ({ theme }) => theme('width'),
minWidth: ({ theme }) => theme('width'),
width: defaultTheme.height,
},
},
}In practice, a site’s design system will likely have additional specific heights and widths. Add those with extend. A real configuration might look something like
const defaultTheme = require('tailwindcss/defaultTheme');
/** @type {import('tailwindcss').Config} */
module.exports {
theme: {
extend: {
height: {
...defaultTheme.width,
80: pxToRem(80),
220: pxToRem(220),
},
maxHeight: ({ theme }) => {
...theme('height'),
110: pxToRem(110),
},
minHeight: ({ theme }) => {
...theme('height'),
// …
},
maxWidth: ({ theme }) => {
...theme('width'),
// …
},
minWidth: ({ theme }) => {
...theme('width'),
// …
},
width: {
...defaultTheme.height,
// …
},
},
},
}Em values. Keep the unit in the key— don’t assume collaborators and/or future me will intuit “no unit means such-and-such a unit”. Override, don’t extend — don’t want IDE suggestions slipping me into using letter spacings beyond what’s in the design.
theme: {
letterSpacing: {
'0.01em': '0.01em',
'0.02em': '0.02em',
},
},Line height keys are the same as their respective values. If the value has a unit, the key does too. Note that the values are strings not numbers (1: '1').
const lineHeight = {
// without units
1: "1",
3: "3",
// with units
"24px": pxToRem(24),
};I translate Tailwind’s default theme’s pixel screens values to rems, for better accessibility.
Read More
I’ve written up my approach in Convert Tailwind’s default screens from px to rem.
Values are rem written as pixels in a pixels-to-rem function (see Sizing). Keys are pixel values without the unit.
Exception: following Tailwind defaults, I sometimes use the key px instead of 1px for the value '1px'.
import { fluidLength } from "@olets/css-fluid-length";
// …
theme: {
spacing: {
0: '0',
'px': '1px',
4: pxToRem(4),
// …
},
},Fluid values use my css-fluid-length utility, and get keys with breakpoints.
import { fluidLength } from "@olets/css-fluid-length";
// …
theme: {
spacing: {
// …
'from-16-to-32': fluidLength(theme('spacing.16'), theme('spacing.32')),
// …
},
},I sometimes inline CSS variable spacings as arbitrary classes pt-[--my-var], but I prefer to add them to the config with semantic keys. For example 'mobile-header' to set the mobile header’s height and the mobile page content’s top padding.
theme: {
spacing: {
// …
'mobile-header': 'var(--mobile-header-height)',
// …
},
},See Height and Width
CSS which targets HTML elements goes in an addBase() plugin in the Tailwind config’s plugins. It could go in a CSS file imported into the index CSS file, or in a standalone plugin file imported into the Tailwind config file, but that’d be one more file to know about and refer to.
In plugins, I prefer @applying Tailwind classes over vanilla CSS (e.g. '@apply hidden': {} not display: 'none'), including using dynamic @apply keys.
If Tailwind doesn’t cover the styles I need, I use arbitrary CSS rather than applying an arbitrary Tailwind class (e.g. counterReset: 'my-counter' not '@apply [counter-increment:my-counter]': "",).
One exception is styling content. I often find Tailwind’s .content-[<val>] CSS variable approach weightier than I need, so I will use content: '""' over '@apply content-[""]': {}.
const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function ({ addBase }) {
const linkInteractionClasses = "transition-… focus-visible:… hover:…";
const linkClasses = `${linkInteractionClasses} …`;
addBase({
/**
* custom reset
*/
html: {
"@apply scroll-smooth motion-reduce:scroll-auto update-slow:scroll-auto":
"",
},
a: {
[`@apply ${linkClasses} …`]: "",
},
img: {
"@apply w-full": "",
},
summary: {
[`@apply ${linkInteractionClasses} …`]: "",
},
// …
/**
* end custom reset
*/
// …
});
// …
}),
],
};All custom utilities go in an addUtilities() and/or in matchUtilities()s in the Tailwind config’s plugins, and all custom variants go in addVariant()s or matchVariant()s. If you’re not already writing utility plugins and variant plugins, it’s worth putting in the time to get comfortable with them, even if you aren’t already comfortable with JavaScript; Tailwind’s built-in helper functions make adding new classes about as simple as is possible (read more in Tailwind’s plugin docs).
As with addBase, I prefer @applying Tailwind classes in the config over vanilla CSS. See Styling HTML Elements.
const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function ({
addUtilities,
addVariant,
matchUtilities,
matchVariant,
}) {
addUtilities({
/* … */
});
addVariant(/* … */);
addVariant(/* … */);
matchUtilities(/* … */);
matchVariant(/* … */);
}),
],
};I heavily favor long class values in view files over dedicated component classes. I will occasionally use addComponents(), but consider it a code smell for a need to refactor component files. There probably is a legitimate use for matchComponents, but I find steering clear of it promotes maintainable, understandable component view file architecture. If you haven’t tried going all in on the atomic-classes-in-markup approach the haters love to hate, try it.
That is, I try to always do
<div class="flex flex-col gap-14 max-w-4xl w-full">…</div>and I try to never do (and in fact essentially never do)
<div class="MyComponent">…</div>const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function ({ addComponents }) {
addComponents({
MyComponent: {
"@apply flex flex-col gap-14 max-w-4xl w-full": "",
},
});
}),
],
};The Tailwind team’s typography plugin @tailwindcss/typography is a handy naive solution for projects that don’t include typography design. But the CSS is generates is heavy (it has to cover a lot of conditions and edge cases), and I find its utility classes hard to read. For fully-custom projects, I skip that plugin and add my own CSS with addUtilities(). One less dependency, and complete control.
I’ve given the wrapper utility different names on different projects. .prose to match @tailwindcss/typography, or .rich-text to reflect its common role in styling CMS rich text fields. As of this writing I prefer .raw.
Here again, I @apply Tailwind utilities in the config. See Styling HTML Elements.
plugins: [
({ addUtilities }) => {
addUtilities({
'.raw': {
a: {
'@apply underline text-blue': "",
},
h1: { /* … */ },
ol: { /* … */ },
ul: { /* … */ },
// …
},
})
},
],
Styling VitePress with Tailwind
Configure Tailwind as a PostCSS plugin to use it in VitePress Markdown, JS/TS, and Vue files

Convert Tailwind 3 default screens from px to rem
Improve the desktop experience for users who prefer larger text

Highlight Code Block Lines In Eleventy with Shiki Twoslash
Eleventy Markdown code block line highlighting made simple