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.