Back to articles
June, 5th 2021

Styling Variative Vue.js Components with TailwindCSS

Styling a component that has a lot of variations is not easy with Atomic CSS approach. Let's consider the following example:

/* html */
<!-- AppButton.vue -->
<template>
  <button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
    <slot/>
  </button>
</template>

export default {
  name: 'AppButton',
  props: {
    density: {
      type: String,
      default: 'md',
      validator: (value) => ['sm', 'md', 'lg'].includes(value)
    }
  }
}

Here a AppButton component receives density prop, that determines the padding inside the button. For that to be implemented, we need to apply padding classes conditionally with computed property. So you might find yourself having something like this:

/* js */
...

export default {
  // ...
  computed: {
    buttonClasses () {
      switch (density): {
        case 'sm':
          return ['px-8', 'py-6']
        case 'md':
          return ['px-12', 'py-10']
        case 'lg':
          return ['px-14', 'py-12']
      }
    }
  }
}

This will work up to a certain point in component development while you have a minimal number of such props that require CSS to change. However if you try to add another prop like bg, things get complicated. You could write something like that:

/* js */
...

export default {
  // ...
  computed: {
    buttonClasses () {
      let classes = []
      switch (density): {
        case 'sm':
          classes.push('px-8', 'py-6')
          break;
        case 'md':
          classes.push('px-12', 'py-10')
          ....

      switch (bg): {
        case 'black':
          classes.push('bg-500')
          break;
        case 'white':
          classes.push('bg-100')
          ....

It's also possible to split in into different computed properties, there're other ways. Readability is the issue here and the fact that conditions might be even more complicated. After writing tons of such logic, I found a nice way to keep readability and structure. Let's try something like this:

/* js */
...

export default {
  // ...
  computed: {
    buttonClasses () {
      return [
        ...{
          sm: ['px-8', 'py-6'],
          md: ['px-12', 'py-10'],
          lg: ['px-14', 'py-12'],
        }[this.density],
        ...{
          black: ['bg-500'],
          white: ['bg-100']
        }[this.bg]
      ]
      
      // returns ['px-8', 'bg-500'] with "sm" density and "black" color
    }
  }
}

This way you can keep you class management consolidated in one place. It also works nicely with boolean props:

/* js */
...

export default {
  // ...
  computed: {
    buttonClasses () {
      return [
        // other classes
        ...{
          true: ['border', 'border-400'],
          false: ['border', 'border-300']
        }[this.border]
      ]
    }
  }
}

Together with this and other techniques you can certainly improve experience writing and maintaining design system components while still getting all the benefits from Atomic CSS approach.


Other Posts
July, 28th 2022 Background-aware swiper pagination Some parts of user interface are not background aware, however are meant to be. It’s especially noticeable when working with user-generated content that is different in colors, exposure and size. Take..
March, 31st 2022 Tree-shaking Vuex State in Nuxt Document response The way Nuxt delivers state data from server to client can play a bad joke with your loading performance. All of your Vuex state data on the server will be printed in the response Document as JS objec..