Written by Francois Brill✏️
Tailwind CSS has done wonders for development — it can get you up and running in a matter of minutes. It contains the right building blocks out of the box with options to customize just about anything throughout the system.
If you’ve never built a design system from scratch, it’s easy taking something like Tailwind CSS for granted, especially when it comes to setting up a type-scale, spacing grid, and colors (where Tailwind truly shines). Tailwind’s world-class designers were meticulous when choosing the right color hues and shades capable of making just about anything look great.
Sometimes, you could be given a design with brand colors you could extend in Tailwind or override default colors with anything you’d like when you know those values upfront.
But what happens when you’re not in control of the exact colors that end up in the user’s browser? What if the colors come from the backend or are dynamic and controlled via user input? Do you resort back to doing inline styles? Or perhaps generate separate CSS styles for those use cases outside of Tailwind?
I hope not! I’m here to show you there’s a better way, a solution that’s native to the way Tailwind works, and its extensible API allows us to push it further than the defaults we get out of the box.
Applying dynamic colors with Tailwind CSS
I’m going to showcase how I solved a real-world use case with a tiny SaaS product I built called NodCards. The concept is simple — it’s your digital business card. With NodCards, your details can be featured on a personal landing page, shared in your email signature, a link-in-bio on social media, or any other place you’d like to share your information.
NodCards is a good example because the user can choose any color as the primary design color, which requires NodCards to adapt dynamically. I was set on using Tailwind CSS for the styling, so how did I do this?
With this context in mind, and knowing how Tailwind classes work, we want to target the text color text-{primary}
for the person’s name, as well as a background color bg-{primary}
for the buttons, and maybe a bit more for hover effect.
Here, the name needs the primary text color, and the buttons need the primary background color.
Your first thought might be, thank goodness for the just-in-time (JIT) compiler, which can dynamically compose styles like bg-[#6231af]
. But I would argue that it’s not a real contender for what we have to do here, as you’ll soon see why.
I had a few considerations and requirements to make this work for my use case:
- How can I apply dynamic colors without having to change markup (CSS classes)? I could potentially get thousands of users wanting different colors, so this shouldn’t add any overhead in the mark-up or stylesheet as it scales
- How can I create various shades of a single primary color?
- How can I determine the best accessible color to show on top of the dynamic primary color?
Based on these criteria, I put together a short demo of the project. To help visualize, I made the demo interactive by adding a color picker to simulate the dynamic changing of color in the UI. As you can see, it changes instantly without affecting the stylesheet and/or mark-up.
Let’s see how we achieved this.
Building a Next.js site using React
The best approach I found was by leveraging the power of CSS variables. The same approach could apply to any language or framework. In my example, I’m building this as a Next.js site using React, but the concepts are transferable. Here’s the full source code.
Writing helper functions
Most of the time, we’ll be provided with a hex color from the backend. In this case, I wrote a quick helper function to help with getting our hex colors from the backend into the right RGB format.
The second utility function we need is a method to get the accessible color that is of high contrast (according to the WCAG guidelines) to display on top of the dynamic primary color. I created a utilities file to centralize my helper functions that we can use later on:
// utils/index.js
/////////////////////////////////////////////////////////////////////
// Change hex color into RGB /////////////////////////////////////////////////////////////////////
export const getRGBColor = (hex, type) => {
let color = hex.replace(/#/g, "")
// rgb values
var r = parseInt(color.substr(0, 2), 16)
var g = parseInt(color.substr(2, 2), 16)
var b = parseInt(color.substr(4, 2), 16)
return `--color-${type}: ${r}, ${g}, ${b};`
}
/////////////////////////////////////////////////////////////////////
// Determine the accessible color of text
/////////////////////////////////////////////////////////////////////
export const getAccessibleColor = (hex) => {
let color = hex.replace(/#/g, "")
// rgb values
var r = parseInt(color.substr(0, 2), 16)
var g = parseInt(color.substr(2, 2), 16)
var b = parseInt(color.substr(4, 2), 16)
var yiq = (r * 299 + g * 587 + b * 114) / 1000
return yiq >= 128 ? "#000000" : "#FFFFFF"
}
The getAccessibleColor
function works by converting the RGB color space into YIQ, as explained in calculating color contrast. For our use case, we needed a reliable method for the text color to go on top of our primary color.
Using CSS variables
With our helper functions in place, we convert our dynamic primary color from our backend to an RGB format that can be used in our CSS variable. We then get the a11y
(accessibility) color.
I structured my function to receive a second param for the type of color I’m declaring. This allows me to reuse the function for any combination of CSS variables I would want to declare. Adding a secondary, accent, or any other color into the mix would follow the exact same logic.
Changing the format into RGB is a crucial step. The main reason we need these colors in RGB format is that when we compose our new colors through Tailwind CSS, we can add an alpha layer for RGBA colors. This ensures all bg-opacity
and text-opacity
classes would work, and we could get various shades of our dynamic color by working with the opacity layer as well.
With our primaryColor
and a11yColor
in RGB format, we need to declare a CSS variable scoped to the root of our HTML document. Adding this on :root
means we’ll have access to it anywhere we use CSS classes within the DOM.
// pages/index.js
const primaryColor = getRGBColor("#6231af", "primary")
const a11yColor = getRGBColor(getAccessibleColor("#6231af"), "a11y")
<!-- ... -->
<!-- ... -->
<Head>
<style>:root {`{${primaryColor} ${a11yColor}}`}</style>
</Head>
For now, we’ve just hard-coded this to #6231af
, but this is easy to replace as a single entry point for any dynamic color later on.
Understanding how the alpha channel in Tailwind CSS works
Any color in Tailwind CSS is declared by immediately declaring a --tw-bg-opacity: 1;
utility in that class. Any colors declared are then made into RGBA values, where it uses the --tw-bg-opacity
value, as declared for the alpha channel.
If you just declare a color, that color would be solid, but when we declare bg-opacity-{value}
, Tailwind CSS re-declares that alpha channel, enabling us to achieve various levels of opacity with our dynamic color.
This is actually very clever from the guys at Tailwind CSS, as there is no way to declare or use a text or background-only opacity in CSS. The only way to achieve this is with the alpha channel in RGBA, and, by following the pattern they’ve laid out, we’re leveraging the full potential of their color system.
Configuring Tailwind CSS
At this point, we have a CSS variable declared in our HTML (which could be connected to our backend). The next step is to link that CSS variable to some Tailwind CSS classes to use.
To achieve this, we have to focus on the tailwind.config.js
file, which is where all the magic happens. Tailwind allows us to assign colors as a function instead of a string to get access to the internal Tailwind opacity utility. This is something that we’ll use a couple of times, so it’s best to extract this into a reusable function at the top of our tailwind.config.js
:
// tailwind.config.js
function withOpacity(variableName) {
return ({ opacityValue }) => {
if (opacityValue !== undefined) {
return `rgba(var(${variableName}), ${opacityValue})`
}
return `rgb(var(${variableName}))`
}
}
First off, we receive the opacity value on colors from Tailwind that we can use with our RGB-formatted color as an alpha channel. Because this might not be set, we perform a quick check: if it’s undefined
, we return it without any alpha.
In either case, we simply assign the value of our CSS variable to the rgb
in this format. This is now ready to use when composing our colors.
Next, we set up our new colors by extending our theme:
// tailwind.config.js
theme: {
extend: {
textColor: {
skin: {
primary: withOpacity("--color-primary"),
a11y: withOpacity("--color-a11y"),
},
},
backgroundColor: {
skin: {
primary: withOpacity("--color-primary"),
a11y: withOpacity("--color-a11y"),
},
},
ringColor: {
skin: {
primary: withOpacity("--color-primary"),
},
},
borderColor: {
skin: {
primary: withOpacity("--color-primary"),
a11y: withOpacity("--color-a11y"),
},
},
},
},
I like to nest these utilities with my own prefix keyword, skin
. This is optional, but it comes in handy when you are typing out classes, especially when you use something like the Tailwind VSCode extension with IntelliSense, which would pick up and show all your new classes.
We extend our theme with the textColor
and backgroundColor
as planned, as well as add the ringColor
and borderColor
variations so that we have our dynamic custom color available on those utilities as well.
Seeing dynamic colors in action
With this in place, Tailwind can generate a bunch of new classes for us. We can just start using any of these new classes in our markup.
Now, let’s change the text color to our new dynamically set primary color.
<p className="... text-skin-primary">
Jane Cooper
</p>
On the buttons, let’s set the background to the primary with a 30 percent opacity, which allows us to use the primary text color. We can then make the background solid on hover and switch to our accessibility safe color for the icon on hover.
We can also use the border color and focus ring colors all set in our primary dynamic color.
<a href={link.href} target="_blank" className="... bg-skin-primary bg-opacity-30 text-skin-primary hover:bg-opacity-100 hover:text-skin-a11y border-skin-primary focus:ring-skin-primary">
<link.icon className="h-5 w-5" />
</a>
Conclusion
Looking back at our requirements, we were able to dynamically set our primary color without changing markup, get different shades of our primary color, and maintain a good contrast ratio when displaying anything on top of our dynamic primary color.
Have you done something similar? Let me know in the comments below!
Is your frontend hogging your users' CPU?
As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web apps — Start monitoring for free.
Top comments (0)