Fixing once and for all...

How to have Perfect Contrast of Text Color on Any Background in TailwindCSS

A screenshot of the generated shades via
Francesco Di Donato
Francesco Di Donato
on 4 min read

Table of Contents

The Problem

In recent years I have developed a good number of websites. The basic structure is usually similar among them, but there are some tasks that always require a lot of effort. And, coincidentally, they are precisely the most tedious and complicated ones. Especially styling and choosing the colors of the website.

There are quite a few palette generators around the web, but all of them leave you with just a bunch of hexs and rgbs — it’s up to you to join those into your system.

As if managing a theme was not enough, you were perhaps required to add the dark mode! The TailwindCSS team proposes to do this by wandering around the template, adding classes prefixed with dark: to instruct each element about its appearance when the selected theme is dark.

This is imho not optimal since, in addition to bloating the template even more, it would require manual work should I be asked to change some colors. And what if I want a third or even fourth theme?

The Real Problem

I’ve been living with this problem peacefully until, after spending too many days hunting for html tags to slightly change their shades, I realized that, alas, the text was no longer readable - it lacked contrast.

Lighthouse accessibility check, unconcerned about my emotional state, lowered the score.

I don’t want to have to think about this problem anymore.

The Solution

I have come up with a system that allows for an unlimited number of themes, each with an unlimited amount of colors. Most importantly, that ensures that the text is always readable on any color.

Basic usage involves configuring at least two themes, light mode and dark mode.

Each theme requires at least five colors. They are background, neutral (useful for card backgrounds), primary, secondary and accent.

No one is stopping you from adding others like success, warning, error… you name it — it is completely configurable!

For each of them, the TailwindCSS utilities and components are produced and available. It means you can not only add a bg-secondary-300 here and a border-t-neutral-500 there, but you get fancy stuff for free — IDE autocomplete included:

<div class="bg-gradient-to-br
  from-primary-300 via-accent-500 to-secondary-700

Text perfect contrast

Thanks to a simple plugin (no need to install anything), some contrast dedicated classes are generated (and available in the IDE autocomplete). They follow the pattern text-wacg-<color>-<shade>.

This text will always be readable

This is easily obtainable with my free online tool.


Read more about the other convenient benefits:

Framework agnostic

It is adoptable in any framework: React, Next.js, Vue, Nuxt, Angular, Svelte, SvelteKit, Sapper, Laravel, Spring, Rails, Django, Express, Hugo, Solid.js, Astro.js, Preact, Ember.js, Alpine.js, LitElement, JQuery and any other I may have omitted.

Zero dependencies

I have opted for a dependency-free solution. As a developer, I do not like to install packages unless it is strictly necessary. Mainly because I know how dangerous it can be — supply chain attack gettin’ real!

This solution is not a black box, and a few paragraph below you can appreciate how simple yet powerful it is.

CSS Only

This solution minimizes JavaScript usage at runtime, leveraging CSS for the majority of heavy lifting.

IDE autocomplete

All the palette colors are utilized to generate all the TailwindCSS utilities (bg-primary-500, border-l-neutral-300, etc…), complete with IDE autocompletion. Additionally, any unused elements are purged by TailwindCSS, ensuring they are not present in the final CSS output.

Browser support

The system is based on two CSS features:

  • var, which allows the declaration and usage of cascading variables in stylesheets, has support on 97.32% of browsers.
  • calc, which allows to calculate values directly into CSS, is supported in 98.19% of all browsers.

If you are intrestend in how this strategy works, read the next post - otherwise otherwise you can already use the free online tool.


Q: Why do I have to explicitly set the text-wacg class? Couldn’t it be implicit in the background one?

A: It could, but I learned to appreciate explicitness.