Read about our latest Free Tool:

Perfect Contrast of Text Color on Any Background

An easy way to ensure

Always visible Text on Any Background with TailwindCSS

A screenshot of copy-paste snippet obtained via https://easypagego.com/tools/tailwindcss-accessible-colors/
Francesco Di Donato
Francesco Di Donato
on 5 min read

In the previous post, I discussed the benefits of effectively using two powerful CSS features: vars and calc. In this post, we’ll delve into how to best utilize these features to ensure that your text remains consistently readable against any background color.

This post aims to simplify understanding the system. Manually writing the necessary files isn’t practical—at least, I wouldn’t want to do it. That’s why I created the Free Online Tool.

Table of Contents

The Goal

We aim for a setup where the same class generated by TailwindCSS applies different colors depending on the theme.

<div class="bg-primary-500 text-wacg-primary-500" />
A

The alternative proposed by the official TailwindCSS team looks something like this:

<div class="bg-blue-500 dark:bg-purple-500 text-black dark:text-white" />

If you’re thinking, Well, that’s not too bad, try to imagine a real-world project. Expand the same concept not just to the background and text, but also to borders, shadows, and gradients

Now, suppose you need to change a color at some point—the text color might no longer match, forcing you to adjust that as well! Madness!


Single Point of Truth

We will define multiple themes, each with its own set of colors. Regardless of the format we use, it is crucial that the state resides in one, and only one, place. This state should be read by TailwindCSS so it can generate all the necessary classes (bg-*, text-*, border-*, etc.).

Fortunately, TailwindCSS allows using CSS variables in the tailwind.config.js.

Instead of defining our colors like this:

We can directly use CSS variables following a specific syntax and utilizing HSL.

However, this means we need to define the CSS variables all at once.

It is preferable to define the three components of HSL separately so that each can be read individually.

Automatic Contrast of Text Color

Using JavaScript, it is possible to read the values of CSS variables, extract the third value of the tuple—lightness—and, with some calculations, determine whether the text should be light or dark.

If it can be done with CSS only, it should be.

Fortunately, we can perform some simple computations directly in CSS using calc. This is executed off the main thread, resulting in a significant performance improvement. As of this writing, it is supported by 98.19% of browsers.

Invert

Here’s the trick: we use a variable called threshold, which will determine at what background lightness level it becomes necessary to invert the text color.

The color is set with a hue of 0 and saturation of 0—hence black. However, the final lightness of the text is the result of:

  • subtraction between the background’s luminosity and the threshold;
  • inversion by multiplication for -100%.
Am I visible?

When you play around with the widget, you’ll see that the text changes color automatically. This happens when we tweak the lightness a bit. It’s a handy trick because it means we don’t have to worry about manually adjusting text colors.

Plus, this method makes our design flexible. So, if we decide to change the colors or layout later on, the text will still look good without us having to make lots of changes.

In short, using this method makes our websites easier to manage and more dev-friendly.

Browser compatibility

A note about Safari on iPhone: it requires hue to be provided with the deg suffix. Also, requires the specific casting of saturation and lightness to percentages. We can’t define these variables as percentages from the start because it would prevent us from subtracting luminosity and threshold.

calc(42% - 69%) is invalid.

calc((42 - 69) * 1%) is valid.

Therefore, while the following syntax is not supported in Safari for iPhone:

This other syntax, though less intuitive, is supported by the vast majority of browsers. Am I wrong? Please contact me

Clearly, this is something that becomes very difficult to handle manually. That’s why I built the Free Online Tool.

Setup

Finally, let’s look at the minimal changes needed to make this mechanism work.

tailwind.config.js

What we want from TailwindCSS is for it to generate its beloved classes. It’s not its concern what color each of them points to.

For simplicity, let’s assume we only have two colors: background and primary.

This would ensure that classes like bg-background and text-primary are generated, but not bg-background-300 or text-primary-700. Achieving the latter is straightforward: simply assign an object whose keys are the shades, and use a different CSS variable for each lightness level—this is the default behavior of the Free Online Tool.

<head>

I recommend adding the following elements to the <head> tag of every page. You probably use a framework that provides some way to define a root layout.

To keep themes consistent across your app, store them as a JavaScript object. This way, you can easily access themes from anywhere in your app.

And, in a <script> tag following the previous one, we can iterate through the requested theme and dynamically apply all the CSS variables.

For simplicity, this script directly applies the “light” theme. The Free Online Tool provides a snippet for ThemeManager, a class that automatically:

  • Retrieves the previously saved theme from localStorage, if available.
  • Otherwise, checks the browser’s preference via window.matchMedia('(prefers-color-scheme: dark)').

Additionally, it’s globally accessible (with TypeScript’s declare) and allows for programmatically changing the theme. It works with any framework (React, Svelte, Vue, Angular, etc.) and can be easily integrated into any reactivity system.

text-wacg-* TailwindCSS Plugin

Let’s get to the focal point. Regarding text contrast optimization, I could have opted for an implicit solution, where the class that sets the background also sets the appropriate text color. However, I prefer the explicit approach. That’s why I’ve created a very simple TailwindCSS plugin that adds these classes.

As they are generated through plugins, these classes are available in the autocomplete of your IDE!

It simply iterates for the colors defined in tailwind.config.js and for each one adds the class called according to the text-wacg-<color-name>-<shade> pattern, using the appropriate lightness in the text inversion calculation.

Don’t forget to register the plugin.


As already mentioned, I don’t think anyone has to write this configuration by hand. The Free Tool Online allows you to define themes and colors for each. It also intuitively allows you to set the threshold for each color. Finally just apply the snippets and copy paste where indicated.