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.