Stateful Styles With Tailwind and Vue

🗓 March 11, 2020

Vue & Tailwind

Tailwind and Vue seem to be a match made in heaven. Pairing Tailwind's utility classes and Vue's dynamic data is a recipe for expressive, state driven component styling. Like this...

Simple Example

<template>
  <button
    :class="btnClass"
    class="p-4 rounded shadow"
  >
    Yay!
  </button>
</template>

<script>
export default {
  name: 'CoolButton',
  props: {
    hasError: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    btnClass () {
      return this.hasError ? 'bg-red-100 hover:bg-red-200' : 'bg-green-100 hover:bg-green-200'
    }
  }
}
</script>

This pattern is where Vue and Tailwind really shine. In the example above we have a (very very simple) button component, that accepts a hasError boolean prop. We use our btnClass computed property to choose a Tailwind utility class, and bind that to the class attribute of the <button> element in our template markup.


Now obviously this is a very simple example, but we can imagine this approach scaling up to include more complex state. Using our state we ultimately want to output a string that includes the appropriate Tailwind utility classes given our state. Thus we do things like this:

More Complex Example

<template>
  <button
    :class="btnClass"
    class="p-4 rounded shadow"
  >
    Yay!
  </button>
</template>

<script>
export default {
  name: 'CoolButton',
  props: {
    hasError: {
      type: Boolean,
      default: false
    },
    isDisabled: {
      type: Boolean,
      default: false
    },
    hasBorder: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    appLoading () {
      return this.$store.state.appLoading
      // this is just a made up value from our vuex store
    },
    loadingStyles () {
      return this.appLoading ? 'bg-gray-400 text-grey-200' : ''
    },
    errorStyles () {
      return this.hasError ? 'bg-red-100 hover:bg-red-200' : 'bg-green-100 hover:bg-green-200'
    },
    disabledStyles () {
      return this.isDisabled ? 'cursor-not-allowed opacity-25 bg-grey-100' : ''
    },
    borderStyles () {
      return this.hasBorder ? 'border border-gray-500 text-gray-500 hover:text-white hover:border-transparent' : ''
    },
    btnClass () {
      // now we just concatenate all of our state specific styles
      // into  a single string!
      return `${this.loadingStyles} ${this.errorStyles} ${this.disabledStyles} ${this.borderStyles}`
    }
  }
}
</script>

Why?

1) Less CSS, more consistent styling.

Without Tailwind you end up doing stuff like this

<template>
  <button
    :class="hasError ? 'btn__err' : 'btn'"
    class="p-4 rounded shadow"
  >
    Yay!
  </button>
</template>

<script>
export default {
  name: 'CoolButton',
  props: {
    hasError: {
      type: Boolean,
      default: false
    }
  }
}
</script>

<style scoped>
.btn {
  // some rules 
}

.btn__err {
  // more rules
}
</style>

This pattern isn't bad, but it does require you to write a new CSS class to match the button state (or pull in some existing global class). I for one, like to reason about my styles from within the component that I'm working in. You can also see how in a more complex example, we end up writing even more classes to match the state, and mapping those classes

2) Even More Testable

This approach adds an additional layer of unit-testability to your stateful styles. Not only can you check that your element has the appropriate classes applied, like so (using vue-test-utils):

import { mount } from '@vue/test-utils'
import CoolButton from './CoolButton.vue'

const wrapper = mount(CoolButton, {
  propsData: {
    hasBorder: true
  }
})
expect(wrapper.classes()).toContain('border border-gray-500')

But you can also access/validate the computed properties that are used to create your class strings, like this!

import { mount } from '@vue/test-utils'
import CoolButton from './CoolButton.vue'

const wrapper = mount(CoolButton, {
  propsData: {
    hasBorder: true
  }
})
expect(wrapper.vm.borderStyles).toContain('border border-gray-500')

This doesn't mean that you shouldn't test by inspecting the DOM element, but just adds another path.

3) "Full Stack" Friendly

I love CSS. But not every developer does, and more importantly.... not every developer feels comfortable contributing to a mature CSS codebase. Tailwind gives developers a clear path for styling new features without worrying about muddying up global styles, or breaking hard conventions.