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.