written by Andre Liem
16/01/2018

Create your own Vue.js Tooltip component with some basic animations.

In this tutorial you'll learn how easy it is to build the beginnings of a simple reusable tooltip component. I'll cover basic topics like component templating, props, slots, transition with utility based CSS courtesy of Tailwind CSS. This tutorial is targetted towards beginner to intermediate developers who have used Vue.js and are comfortable with CSS.

Introduction - Why not use an existing component?

If you're like me, you've probably ran into the situation where you need a specific component, google or search github and find a few that sound great. You start with the most popular, npm install, try it out and hope things work. Often times, you find that it doesn't quite work. Sometimes it requires a version of Vue.js or some library that you are not using yet, or perhaps a dependency you prefer not to have. Most cases, the problem is that the component does not do exactly what you need to do and the API provided is not enough to get you there. So you end up spending more time trying to install and configure a 3rd party component when you could have just been building it yourself.

More recently, I found myself trying various tooltip components. I found lots of great components, some written by very reputable creators, but not surprisingly it seemed difficult to get the one feature I needed which was to populate the tooltip async based on an API call.

So I decided I should get in the habit of building it myself, especially when the feature is not that difficult. Once you have the CSS, the actual structure of building a reusable Tooltip component for yourself is quite easy with Vue.js. The features provided require very little code or hackery to make it work well.

In this tutorial, I'll review a basic tooltip component I built in Codepen which you can use for your own projects. No need to npm install or worry about future updates, take this code, drop in in your project and maintain/tweak it as you need.

Disclaimer: The code has not gone thorugh any extreme browser testing. You might have to modify the CSS to make it backwards compatible with legacy browsers.

Start at the End

I prefer building compoents by starting with the finished UI or static HTML + CSS. Then I work from there towards the internals of what makes it work. Here is a picture of what the final product will look like.

Goal
As you can see we have two buttons which will show a tooltip when hovered over. My goal was to make it possible to control the transition / animation of the tooltip as well using a Bounce and Fade transition.

Also worth noting that the CSS for this was taken directly from the excellent Tailwind CSS documentation. With Utility CSS you can quickly prototype components because you won't be spending time creating or thinking about semantic names for classes.

More about Functional CSS

Lately I've been shifting towards more Functional CSS that essentially revolves around using a combination of CSS classes inline instead of creating actual semantic classes. This CSS approach was popularized by Tachyon CSS and more recently has taking new life with Tailwind CSS. I was really inspired to try out Functional CSS when I read this post from Adam Wathan CSS Utility Classes and Seperation of Concerns

The reason I mention this approach is because it's extremely fast to prototype components using inline styling, especially for developers which rely on hot reloading and/or designing in the browser. Also, it doesn't necessarily exclude BEM or a more traditional semantic approach to CSS naming. Once a prototype is finalized, you can refactor or extract common CSS into specific classes if you please. In my experience, I've found it surprising how little I actually do this.

Here is what the UI code looks like for the UI using this approach.

<body class="p-4">
  <script id="tooltipTemplate" type="x/template">
    <div 
    class="absolute bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md" 
    style="height: auto; min-height: 200%; top: 105%; left: 0%; min-width:220px;" 
    role="alert">
      <div class="flex">
        <div class="py-1"><svg class="fill-current h-6 w-6 text-teal mr-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM9 11V9h2v6H9v-4zm0-6h2v2H9V5z"/></svg></div>
        <div>
          <p class="font-bold">A tooltip!</p>
          <p class="text-sm">Built with Vue.js & Tailwind CSS.</p>
        </div>
      </div>
    </div>
  </script>
  
  <script id="tooltipButton" type="x/template">
     <div class="relative inline-block bg-blue hover:bg-blue-dark text-white font-bold py-4 px-4 rounded-full">
      <Tooltip></Tooltip>
    </div>   
  </script>
  
  <div id="app">
    <TooltipButton :transition="'bounce'">Bounce</TooltipButton>
    <TooltipButton :transition="'fade'">Fade</TooltipButton>
  </div>
  
</body>

There are 3 key things going on here. There is one template that represents the tooltip that will appear tooltipTemplate. The other template tooltipButton is for the buttons that we saw at the beginning.

Take sometime to look at the CSS here, it's a lot but actually most if not all of it should make sense. For example, absolute means position absolutely, relative means you guessed it, position relative etc... there's also some inline css for setting the dimensions of the tooltip. This could easily be dynamic but for the purposes of this demo I didn't go that far ;).

The actual HTML in there <div id="app"></div> is the container for what we'll see and drops in 2 components which are buttons with tooltips.

Setting up the JS

Next, we need to create a few JS components to bring life to these templates.

<script type='text/babel'>
let Tooltip = Vue.component('tooltip',{
  template: "#tooltipTemplate",
  }
})

let TooltipButton = Vue.component('tooltipbutton', {
  template: '#tooltipButton',
  components: {
    Tooltip
  }
})

let App = new Vue({
  el: '#app',
  components: {
    TooltipButton
  }
})
</script>

Pretty self explanatory here. We create 2 components, linked to the templates we defined before. The TooltipButton component uses the actual Tooltip component to popup content. Lastly, our app must reference the TooltipButton so that we can drop it into the HTML.

Peekaboo

Next, lets find a way to make the tooltip show only when the button is hovered over by defining a data Boolean property called showTooltip. All this data property does is set the v-show on the tooltip to show when appropriate.

Below are the relevant updates needed.

<script type='text/babel'>
let Tooltip = Vue.component('tooltip',{
  template: "#tooltipTemplate",
  props: {
    show: {
      type: Boolean,
      required: true
    }
  }
})

let TooltipButton = Vue.component('tooltipbutton', {
  template: "#tooltipButton",
  data () {
    return {
      showToolTip: false
    }
  }
})
</script>

<script id="tooltipTemplate" type="x/template">
  <div v-show="show">
  </div>
</script>

<script id="tooltipButton" type="x/template">
<div 
  v-on:mouseover="showToolTip=true" 
  v-on:mouseleave="showToolTip=false"
>
  <slot></slot>
  <Tooltip :show="showToolTip"></Tooltip>
</div>   
</script>

Props + V-Show = Hide / Show Tooltip

When a button is hovered over, we set the showToolTip property to true or false as needed which is passed to the prop show. The prop show is defined at the top and simply wraps the entire component with the v-show.

Slot = Button Text

Notice there's a slot in the tooltipButton template? All this does is allow us to display the content from the parent componet that is using the button.

e.g. <Tooltipbutton>This Goes in the Slot</Tooltipbutton>

From here we have the basics working! Lets make it a bit nicer by adding in some animations with Vue.js transitions.

Transitions + CSS = Animating it

If you know how to use transitions, this should be a walk in the park. If you don't, I recommend reading the official docs. The only CSS that we include outside of the template is for animating the tooltip.

What we need to do is simply wrap the tooltip in a transition which pulls the transition name from a prop. We use a prop so that our code is a bit more flexible, or so that the calling code can specify what type of transition is desirable for the tooltip.

Here is the final product below.

<body class="p-4">
  <script id="tooltipTemplate" type="x/template">
    <transition :name="transition">
      <div v-show="show" class="absolute bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md" style="height: auto; min-height: 200%; top: 105%; left: 0%; min-width:220px;" role="alert">
        <div class="flex">
          <div class="py-1"><svg class="fill-current h-6 w-6 text-teal mr-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM9 11V9h2v6H9v-4zm0-6h2v2H9V5z"/></svg></div>
          <div>
            <p class="font-bold">A tooltip!</p>
            <p class="text-sm">Built with Vue.js & Tailwind CSS.</p>
          </div>
        </div>
      </div>
    </transition>
  </script>
  <script id="tooltipButton" type="x/template">
     <div v-on:mouseover="showToolTip=true" v-on:mouseleave="showToolTip=false" class="relative inline-block bg-blue hover:bg-blue-dark text-white font-bold py-4 px-4 rounded-full">
      <slot></slot>
      <Tooltip :transition="transition" :show="showToolTip"></Tooltip>
    </div>   
  </script>
  <div id="app">
    <TooltipButton :transition="'bounce'">Bounce</TooltipButton>
    <TooltipButton :transition="'fade'">Fade</TooltipButton>
  </div>
</body>


<script type="text/babel">
let Tooltip = Vue.component('tooltip',{
  template: "#tooltipTemplate",
  props: {
    show: {
      type: Boolean,
      required: true
    },
    transition: {
      type: String,
      default: 'bounce',
      required: true 
    }
  }
})

let TooltipButton = Vue.component('tooltipbutton', {
  template: '#tooltipButton',
  props: {
    transition: {
      type: String,
      default: 'bounce',
      required: false
    }
  },
  components: {
    Tooltip
  },
  data () {
    return {
      showToolTip: false
    }
  }
})

let App = new Vue({
  el: '#app',
  components: {
    TooltipButton
  }
}
</script>

Here is the CSS we can include in an external CSS file or within a component if you wish.

<style type="text/css">
.fade-enter-active {
  transition: opacity 0.3s
}

.fade-leave-active {
  transition: opacity 0.5s
}
.fade-enter, .fade-leave-to  {
  opacity: 0
}

.bounce-enter-active {
  animation: bounce-in .5s;
}
.bounce-leave-active {
  animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.5);
  }
  100% {
    transform: scale(1);
  }
}
</style>

Tada!

With that piece of code, we now have a cool little animated tooltip!

Done

You could take this and go to town on making it fit your specific needs. For example, you might want to make it position top, bottom, left, right to the button. That would not be terribly difficult using a prop which conditionally loads in CSS for positioning. Or another alternative solution would be to let the component receive CSS that it applies inline, giving the calling code complete control over the layout. These are the hard decisions you need to make when you share a component with the world as a official package. When you are building it for your own project, I would avoid abstracting too much unless it's solving a clear problem.

If you would like to grab the code, you can visit the Code Pen here
https://codepen.io/andreliem/pen/VyEape

Or if you prefer you can visit the Github repo for this specific Codepen
https://github.com/andreliem/vuesnippets/tree/master/vue-tooltip-comp-slots-transition-props

Hope you found this article useful, if you would like to learn about building more reusable components please subscribe to the newsletter! Articles explaining how to build reusable components will be released 2-3 times per month!