DEV Community

Cover image for How to have Perfect Contrast of Text Color on Any Background in TailwindCSS
Francesco Di Donato
Francesco Di Donato

Posted on • Originally published at

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

You can read the interactive version of this blog post!

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
Enter fullscreen mode Exit fullscreen mode

A square with a fancy colored background and shadow

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>.

You can try real-time widgets on my blog post!

The widget the helps appreciate the power of this strategy

<div class="bg-primary-500 text-wacg-primary-500">
    This text will always be readable
Enter fullscreen mode Exit fullscreen mode

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:


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.

Top comments (1)

dev_michael profile image
Michael Amachree

This is fantastic! It would be even better if you could provide instructions on how to integrate this functionality into a SvelteKit project, both for new projects and existing ones. Ideally, the instructions would follow established Svelte practices. A React guide for our React-loving friends would be amazing too!