Working with reactive arbitrary values in TailwindCSS
Tailwind's arbitrary values are a great way to use custom values in your design that are unavailable through the included utility classes. Still, they get tricky when using values based on user input or any other variable comming from your JavaScript code. In this guide, we will explore the optimal way of using dynamic arbitrary values in any Tailwind project.
Disclaimer
I will use Vue.js for my examples, but I promise the core concepts will work the same in any other technology you choose, like React, Svelte, or even vanilla JavaScript.
Working with dynamic classes in general
If you have ever tried to construct a Tailwind utility class using JavaScript, you already know it won't work by default. This is how Tailwind should work by design. The build process analyses your code, looking for statically detectable Tailwind classes to include in your final stylesheets. It means all the unused classes will go to the chopping block and be purged from existence. This is how Tailwind keeps your CSS files performant.
Let's see the following example where our Button's background is based on the colour prop.
// components/Button.vue
<script setup>
const props = defineProps(['color'])
</script>
<template>
<!--
note:
these classes will be purged during build, and the bg color won't show
-->
<button
:class="`bg-${color}-600 hover:bg-${color}-500`"
>
<slot></slot>
</button>
</template>
The above code won't work because Tailwind will not find any utility classes here. Luckily, it's an easy fix. You even have two options to choose from.
Refactoring
I recommend refactoring your code so that the class names are used so that Tailwind can catch them and include them in your styles.
// components/Button.vue
<script setup>
const props = defineProps(['color'])
// note: classes are listed here for Tailwind
const colorVariants = {
blue: 'bg-blue-600 hover:bg-blue-500',
red: 'bg-red-600 hover:bg-red-500',
}
</script>
<template>
<button :class="colorVariants[color]">
<slot></slot>
</button>
</template>
Safelisting
If this is not possible for your project, your other option is to put your classes on a safe list and let Tailwind know that you will definitely need them even if they aren't in your codebase. For this solution, you just put the specific class names in your Tailwind configuration's safelist
option.
// tailwind.config.js
module.exports = {
content: [
'./components/**/*.{vue,js}'
],
safelist: [
'bg-blue-600',
'hover:bg-blue-500',
'bg-red-600',
'hover:bg-red-500',
]
// ...
}
What about arbitrary values?
Let's imagine a square box where the user can modify the size
using a simple range input. Using a constructed class like w-[${size}px]
will not work for the previously mentioned reasons.
So, how can you make this work? Yet again, you are left with two options: either solve the problem without using Tailwind or convert your dynamic class to a static version and move the dynamic part elsewhere.
Solution 1
Your first solution is to simply forget about Tailwind and use inline styling.
// components/DynamicBox.vue
<script setup>
import { ref } from 'vue'
const size = ref(125)
</script>
<template>
<input type="range" v-model="size" min="25" max="250" />
<div class="mt-4 h-64">
<div
class="bg-blue-600 aspect-square"
:style="{ width: `${size}px` }"
></div>
</div>
</template>
Sometimes, this is the easiest and most optimal way to solve your problem, but it has some downsides. In our case, it will work perfectly fine, but you will lose the benefit of combining it with interactive modifiers like hover
and responsive modifiers like lg
.
Solution 2
The problem with dynamic arbitrary values is that you can't use the same workarounds I showed for built-in utility classes. Since they have a constantly changing part, you can't safelist them or present them in your sources. Still, the solution is similar in a way that we have to make our dynamic class statically detectable.
This solution is similar to the previous one, as we have to use inline styles in cooperation with Tailwind, where we set the dynamic values as CSS variables with the combination of Tailwind's square bracket class notation using that variable.
Keep in mind when you use a CSS variable as an arbitrary value, you don't have to wrap your variable in var(...)
as you would in regular CSS.
// components/DynamicBox.vue
<script setup>
import { ref } from 'vue'
const size = ref(125)
</script>
<template>
<input type="range" v-model="size" min="25" max="250" />
<div class="mt-4 h-64">
<div
class="bg-blue-600 aspect-square w-[--size]"
:style="{ '--size': `${size}px` }"
></div>
</div>
</template>
Example
This solution will work just the same as the previous one, but now you have the option to play around with modifiers like lg:w-[calc(var(--size)*2)]
.
Refactor
In case you would like to clean this up a little bit, you can always define a new custom class in your Tailwind configuration using an expected variable like:
// tailwind.config.js
module.exports = {
theme: {
extend: {
width: {
custom: 'var(--size)'
},
},
},
// ...
}
Then, use this class instead in any of your components.
// components/DynamicBox.jsx
<template>
<!-- ... -->
<div
class="bg-blue-600 aspect-square w-custom"
:style="{ '--size': `${size}px`}">
</div>
<!-- ... -->
</template>
Conclusion
As developers, we often fall in love with the convenience and efficiency of the tools we work with. However, it is essential to recognise when a different approach may offer a better solution for our problems. Just because you love using Tailwind and you rely on its utility classes, you should never be restricted by them.