An easy way to ensure
Always visible Text on Any Background with TailwindCSS
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
- Single Point of Truth
- Automatic Contrast of Text Color
- Setup
text-wacg-*
TailwindCSS Plugin
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" />
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 thethreshold
; - inversion by multiplication for
-100%
.
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.