Constantin Druccdruc

Hey, I'm Constantin Druc!

I'm a web developer sharing everything I know about building web applications.

I'm making a course on Laravel Sanctum: MasteringAuth.com

Simplifying Dynamic Classes in Vue with the class-variance-authority library

I hate dealing with dynamic classes in vue.

I have one of those simpleton, 8-bit brains that can't really hold too much information at once, and dealing with dynamic classes requires just that.

Consider the example:

<button 
  class="h-10 px-4"
  :class="{
    'cursor-not-allowed': disabled,
    'text-gray-400': intent === 'text',
    'bg-blue-700 text-white hover:bg-blue-600': intent === 'primary' && !disabled,
    'bg-gray-100 text-black hover:bg-gray-50': intent === 'secondary' && !disabled,
    'bg-gray-100 text-gray-400': intent !== 'text' && disabled,
  }"  
>
  Press me
</button>

The way we usually process this is like:

  • remember the classes on the left side
  • mentally process what happens on the right side
  • if the result on the right is true, the classes will apply
  • forget what those classes were; go back and read them again 🙄

This sucks, but we can make it better!

Flip it back

Our programming brain is very used to the "if-then-else" statement. So it would be great if we could convert the above to something we're already used to. And we can, by using an array!

<button 
  class="h-10 px-4"
  :class="[
    disabled && 'cursor-not-allowed',
    intent === 'text' && 'text-gray-400',
    intent === 'primary' && !disabled && 'bg-blue-700 text-white hover:bg-blue-600',
    intent === 'secondary' && !disabled && 'bg-gray-100 text-black hover:bg-gray-50',
    intent !== 'text' && disabled && 'bg-gray-100 text-gray-400'
  ]"  
>
  Press me
</button>

Our reading process got shorter:

  • process what happens on the left
  • if that is true, apply the classes on the right

I don't know about you, but this is way better!

There's still something I don't like, though.

The conditions on the left can become quite difficult to process. Mainly because we can have all kinds of operators in there: equals, booleans, negations, not equal, includes, doesn't include, etc. All these can come in different combinations, which makes it even harder.

We could try using only the equality operator:

<button 
  class="h-10 px-4"
  :class="[
    disabled === true && 'cursor-not-allowed',
    intent === 'text' && 'text-gray-400',
    intent === 'primary' && disabled === false && 'bg-blue-700 text-white hover:bg-blue-600',
    intent === 'secondary' && disabled === false && 'bg-gray-100 text-black hover:bg-gray-50',
    (intent === 'primary' || intent === 'secondary') && disabled && 'bg-gray-100 text-gray-400'
  ]"  
>
  Press me
</button>

But then the whole thing gets way too long.

Ideally, the "if" part of our problem should be simpler and shorter!

Consider this:

{
  intent: 'primary', 
  disabled: false, 
  class: 'bg-blue-700 text-white hover:bg-blue-600'
}

If you read the above like "intent primary, disabled false, class bg-blue-700..etc" you are 100% correct; that's exactly what it means!

The operator is always "equals" and we still kind of follow the if-then-else statement. If all key:value pairs before class are true, then the classes will apply. Even though the above, technically, is longer than our initial example, it's way easier to read!

This is why I absolutely love the class-variance-authority library. I have no idea why it's named like that, but I love it!

CVA (class-variance-authority)

CVA is a small library, basically a function, that allows us to define variants for the element we want to style.

A simple variant definition has a name and a list of possible values, each with a list of classes that should apply.

{
  intent: { // variant name
    primary: 'bg-blue-700 text-white hover:bg-blue-600', // value and classes
    secondary: 'bg-gray-100 text-black hover:bg-gray-50' // value and classes
  }
}

The above basically translates to:

intent === 'primary' && 'bg-blue-700 text-white hover:bg-blue-600'
// and
intent === 'secondary' && 'bg-gray-100 text-black hover:bg-gray-50'

A compound variant is an object who's key:value pairs represent a variant name and its value. This allows us to apply classes based on different combinations of variants.

{
  intent: 'primary', 
  disabled: false, 
  class: 'bg-blue-700 text-white hover:bg-blue-600'
}

class represents the applied css classes and should never be used as a variant name.

Putting everything together, here's how our initial example would look like using cva:

const buttonClass = computed(() => {
  return cva(
    "h-10 px-4", // default classes which are *always* applied
    {
      variants: {
        intent: {
          text: 'text-gray-400' // applied if intent === 'text'
        },
        disabled: {
          true: 'cursor-not-allowed' // applied if disabled === true
        }
      },
      compoundVariants: [ // classes apply if all conditions are true
        {intent: 'primary', disabled: false, class: 'bg-blue-700 text-white hover:bg-blue-600'},
        {intent: 'secondary', disabled: false, class: 'bg-gray-100 text-black hover:bg-gray-50'},
        {intent: ['primary', 'secondary'], disabled: true, class: 'bg-gray-100 text-gray-400'}
      ]
    }
  )({
    intent: props.intent, // pass variant values. eg: "primary", "secondary", "text"
    disabled: props.disabled // pass variant values. eg: false, true
  });
});

Ugh, I can already see your face 😂;

I'm sure a lot of you are a bit confused; like, hey, this looks like way too much work! You said you had like an 8-bit brain; what's with this long-ass definition???

You are right; this seems quite complex and difficult to write. But trust me, once you try it, it will feel just like tailwind did the first time you used it; it will be weird, possibly a bit confusing, but you’ll grow to love it, and it will forever change the way you write dynamic classes in vue.

If you enjoyed this post, do me a favor and subscribe to my YT channel!

If you end up trying cva, I'm sure you'll love it, so make sure to star the library and share this blog post with the entire universe!