As of
Est. 

Styling with Tailwind CSS

How to use Tailwind CSS to style content in VitePress.

Tailwind CSS is a CSS library that provides CSS utility style classes. Most utility style classes correspond to a CSS property / value combination such as float-left for float: left or pt-1 for padding-top: 0.25rem.

Utility Classes

When CSS came along, we were all trained in semantic class structures. Now, to be hip, you better use classes that convey no semantic meaning whatsoever, but directly and exclusively express details of the styling.

Isn't That Totally Wrong?

Sure, HTML only knew paragraph and image and heading, but we knew better! We defined .blog-post-title and .blog-post-teaser and .blog-post-card and we knew why it had to be that way:

  1. Names are important. If you can't name it, you can't maintain it.
  2. Separation of concerns: the HTML element gets a class and the styling is defined elsewhere.
  3. Simple, understandable, self-explanatory.
  4. Don't repeat yourself.
  5. The HTML5 spec says about values for the class attribute: … authors are encouraged to use values that describe the nature of the content, rather than values that describe the desired presentation of the content.

OK, now let's try the complete opposite:

html
<figure class="rounded-xl md:flex bg-gray-400 md:p-0 p-8" >
  <img class="md:w-48 md:h-auto md:rounded-none w-24 h-24 ...">
  <div class="md:text-left space-y-4 pt-6 md:p-8 text-center ">
<figure class="rounded-xl md:flex bg-gray-400 md:p-0 p-8" >
  <img class="md:w-48 md:h-auto md:rounded-none w-24 h-24 ...">
  <div class="md:text-left space-y-4 pt-6 md:p-8 text-center ">

Hard to decipher abbreviations, styling that is part of the markup, no semantics, just style.

Both Approaches Make Sense

I think both approaches have their advantages, and much depends on the scenarios to which they are applied. Of course, if you want to identify an HTML element as a particular concept, you should use meaningful names. This also has advantages when it comes to identifying these concepts on a page or an entire site, since you can use the class names in selectors.

If you just want to change the styling of a single element, it is very convenient to add CSS directly to the style attribute of this element. But it has its limitations: for example, you can't use media queries or CSS pseudo classes this way. With utility style classes, you can work around these limitations because the class names can contain pseudo classes like hover: and dark: or sm: for small screens. And with utility classes, you usually have to type less than with style attributes.

Applying Tailwind Classes

Anyway: I wanted to be able to use Tailwind on this website, and these are the ways it can be applied.

Tailwind in Vue Components

So, this site uses Vue, and Vue SFCs have templates, and templates have HTML elements, and they can have class attributes, and there you go:

vue
<template>
 <div class="float-right font-mono text-xs text-right 
             text-brand-dark/75 dark:text-brand/75">
  As of {{ lastModified }}<br />
  Est.&nbsp; {{ created }}
 </div>
</template>
<template>
 <div class="float-right font-mono text-xs text-right 
             text-brand-dark/75 dark:text-brand/75">
  As of {{ lastModified }}<br />
  Est.&nbsp; {{ created }}
 </div>
</template>

This is the definition for the dates on the upper right of each page. The <div> is styled with Tailwind classes. As you can guess, the text floats to the right, uses a small, mono-space font, and is right-aligned. The text colors are the 75% opaque variants of the darker brand color for light mode and the normal brand color for dark mode. The link has a different color: this is the 100% opaque brand color that most links use. As a cross-cutting concern this is best defined with a standard declaration, not Tailwind:

css
a {
  text-decoration: none;
  color: var(--vp-c-brand);
}
a {
  text-decoration: none;
  color: var(--vp-c-brand);
}

Tailwind in Markdown

Most of the content on this site is not in Vue components, but in Markdown. But there is no place for utility classes in Markdown, right? In VitePress, it is possible to insert HTML tags in markdown, but a more markdown way is to use the built-in markdown-it-attr plugin. Here are some examples, all rendering text with a colored background:

ApproachText in MarkdownResult
Raw HTML inside markup<span class="bg-blue-500/50">Blue</span> background in markdownBlue background in markdown
attr-plugin with existing (italic) markdown_Orange_{class="bg-orange-500/50"} background in markdownOrange background in markdown
attr-plugin + shortcut_Green_{.bg-green-500/50} background in markdownGreen background in markdown
attr- and span-plugin::Yellow::{.bg-yellow-500/50} background in markdownYellow background in markdown
Different ways to style markdown with Tailwind

The first line of is the somewhat clumsy HTML variant. Of course beside <span> other elements would also work.

The second line of shows how to use the attr plugin. Basically, you use the markdown you already have and add attributes in curly braces. This works with **bold** and _italic_, but also with list items, headings, tables, and others. Inside the braces you can write HTML attributes as usual, e.g. class="..." or even style="...".

Especially for class attributes the attr plugin offers a shortcut: {class="abc def"} can be shortened to {.abc .def}[1] The third line of shows this.

The last line of is where the markdown-it-span plugin comes in handy: if there is no appropriate markdown structure to which the markdown-it-attr plugin could attach, the span plugin can be used to insert arbitrary <span>...</span> pairs as required. Then the attr plugin adds the class to the span. In fact, line four translates to the same structure as line one.

The quotation of the HTML5 spec at the end of the list of good reasons for semantic class names on this page was also styled with Tailwind and the attr plugin like this:

markdown
*<q>...</q>*{.text-blue-900 .dark:text-slate-300}
*<q>...</q>*{.text-blue-900 .dark:text-slate-300}

Due to the pseudo class dark: this works for light mode as well as for dark mode.

How To Enable Tailwind in VitePress?

Tailwind runs integrated in the build process. The advantage is that only those CSS definitions that are really used are also generated and sent to the browser. Imagine how many combinations are possible by multiplying colors, opacity, pseudo-classes and other parts of possible Tailwind class names. It's good, to have them generated only when needed.

The jotting about VitePress shows how to extend VitePress with own CSS. Now here are the contents of that ./styles/tailwind.css file.

css
/* tailwind.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* tailwind.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

The first line activates the preflight feature of Tailwind. This basically removes all the default styling from the HTML elements. I was not prepared for this. It's depressing to see lists or headings with no styling 😱 at all. If that puts you off, you can delete that line as well. I recommend to keep it, as it makes up for the differences between browsers and challenges you to tackle the whole design without defaults.

The @-rules are interpreted by the Tailwind plugin in PostCSS. So we have to tell PostCSS to use this plugin.

js
// postcss.config.js
/* eslint-env node */
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};
// postcss.config.js
/* eslint-env node */
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

And the Tailwind plugin also needs some configuration:

js
// tailwind.config.js 
/* eslint-env node */
module.exports = {
 content: [
  "./docs/**/*.{html,vue,js,ts,md}",
  "./docs/.vitepress/**/*.{html,vue,js,ts,md}",
 ],
 darkMode: "class",
 plugins: [
  // require("@tailwindcss/typography"),
 ],
  theme: {
    extend: {
      colors: {
        "brand": withOpacityValue("--vp-c-brand-triple"),
        "brand-light": withOpacityValue("--vp-c-brand-light-triple"),
        "brand-dark": withOpacityValue("--vp-c-brand-dark-triple"),
  },
 },
};
// tailwind.config.js 
/* eslint-env node */
module.exports = {
 content: [
  "./docs/**/*.{html,vue,js,ts,md}",
  "./docs/.vitepress/**/*.{html,vue,js,ts,md}",
 ],
 darkMode: "class",
 plugins: [
  // require("@tailwindcss/typography"),
 ],
  theme: {
    extend: {
      colors: {
        "brand": withOpacityValue("--vp-c-brand-triple"),
        "brand-light": withOpacityValue("--vp-c-brand-light-triple"),
        "brand-dark": withOpacityValue("--vp-c-brand-dark-triple"),
  },
 },
};

The most important part of this configuration is to tell Tailwind which input files to scan for CSS classes. The darkMode: "class" line is explained in more detail in the jottings about the dark mode.

Tailwind itself has plugins. The typography plugin adds clean styling to standard HTML after preflight. It's primarily intended for article-style content that comes from a CMS or similar source, where you don't have or want full control over the CSS classes. As always with styling, it's a matter of taste. So I tried it and then commented it out.

As an extension of the default Tailwind colors, I also defined colors for the brand. These are the same values used in the VitePress theme.

I had to adopt the structure of the CSS variables.

VitePress defines the custom properties for the colors as rgb() values, but the withOpacityValue function requires separate values for red, green and blue. It uses the space-separated Color Model 4 syntax for rgb values and the slash for opacity. This works only for rgb triples but not for colors like #FF9966.
Thus I defined the brand color in two steps:

css
:root {
 ...
 --vp-c-brand-triple: 156 118 3;
 --vp-c-brand: rgb(var(--vp-c-brand-triple));
 ...
}
:root {
 ...
 --vp-c-brand-triple: 156 118 3;
 --vp-c-brand: rgb(var(--vp-c-brand-triple));
 ...
}

--vp-c-brand custom property is for the VitePress default theme and the --vp-c-brand-triple is for the use with the Tailwind theme.

js
// tailwind.config.js 
...
function withOpacityValue(variable) {
  return ({ opacityValue }) => {
    if (opacityValue === undefined) {
      return `rgb(var(${variable}))`;
    }
    return `rgb(var(${variable}) / ${opacityValue})`;
  };
}
// tailwind.config.js 
...
function withOpacityValue(variable) {
  return ({ opacityValue }) => {
    if (opacityValue === undefined) {
      return `rgb(var(${variable}))`;
    }
    return `rgb(var(${variable}) / ${opacityValue})`;
  };
}

This way I can use Tailwind's standard opacity mechanism for colors. Class names can be postfixed with /<percentage>. This defines the opacityValue and the function uses the rgb triple and the opacity to construct an rgb value with opacity.[2] Looking forward to CSS Color Model 5 which would allow

js
`rgb(from var(--vp-c-brand) r g b / ${opacityValue})`;
`rgb(from var(--vp-c-brand) r g b / ${opacityValue})`;

without the explicit triple.


  1. Likewise {id="myId"}can be written as {#myId} ↩︎

  2. Which was called rgba before CSS Color Level 4. ↩︎