Build a custom tooltip directive with CSS and Vue
Tags: vue, css, tutorials, javascript
Published: 2023-03-02 18:31:06

---
title: "Build a custom tooltip directive with CSS and Vue"
published: true
description: "Let's explore how we can build add a tooltip to your Vue component using directive and CSS."
img: "articles/tooltip/css_tooltip_directive"
tags: ["vuejs", "css", "tutorials", "javascript"]
cover_image: https://res.cloudinary.com/mayashavin/image/upload/v1677065364/articles/tooltip/css_tooltip_directive
canonical_url: https://mayashavin.com/articles/tooltip-directive-css-vue
---

In [the previous post](https://dev.to/mayashavin/build-an-accessible-tooltip-component-efficiently-with-css-and-vue-1p27), we explored how to build an accessible tooltip component in Vue. However, creating an element for a simple tooltip can be overkill. Hence, in the post, we will explore another approach to enhancing a component with a tooltip, using custom directives and CSS.

## Table of Contents

- [Table of Contents](#table-of-contents)
- [What is a Vue directive?](#what-is-a-vue-directive)
- [Prerequisites](#prerequisites)
- [Creating a tooltip directive](#creating-a-tooltip-directive)
- [Registering and using the tooltip in the application](#registering-and-using-the-tooltip-in-the-application)
- [Adding the tooltip effect with CSS](#adding-the-tooltip-effect-with-css)
- [Using directive modifiers for the position](#using-directive-modifiers-for-the-position)
- [Summary](#summary)

---

## What is a Vue directive?

While a Vue component is a fully standalone entity of an application, a directive is more like a decorative and lighter-weight block of code that we can attach to an element to enhance its functionality and appearance on the browser. It allows us to directly add side effects or extra modifications to the related DOM elements. Some common examples of a Vue directive are `v-if`, `v-for`, etc., which we use extensively in any Vue application. 

Besides the built-in directives, Vue also provides us with the Directive API for creating our custom directive. We can define a custom directive as an object of appropriate lifecycle hooks or a function that allows us to interact with the DOM element instance created for the component.

Because our tooltip is mainly about working with classes and CSS and is not likely to change between component updates and mount, we will use the functional approach. Let's get started, shall we?

## Prerequisites

You need to have a working Vue application (see [the official guide for setting up a new Vue project with Vite](https://vuejs.org/guide/quick-start.html#creating-a-vue-application)) and a basic understanding of CSS, and then we can proceed to create our tooltip directive.

## Creating a tooltip directive

To make a custom directive, we first create a new `/directives/tooltip.js` file in our application source folder with the following code:

```js
export function TooltipDirective(el, binding) {
}
```

The `TooltipDirective` function accepts two arguments - the DOM element instance `el` and a `binding` object, which contains the value passed to the directive and other modifiers when using it on a component. For our tooltip, we expect the `binding.value` to be an object with the following properties:

```typescript
type TooltipOptions{
  text: string;
  position: 'top' | 'bottom' | 'left' | 'right';
}
```

We can use the `binding.value` to set the tooltip text and position with the help of classes and the `data-tooltip` attribute. We also add another class - `with-tooltip` - on the target element, as follows:

```javascript
export function TooltipDirective(el, binding) {
    el.setAttribute('data-tooltip', binding.value.text);
    el.classList.add('with-tooltip');

    const position = binding.value.position;
    el.classList.add(`tooltip--${position}`);
}
```

Upon mounting (and updating) the component, Vue will trigger this function and add a new attribute `data-tooltip` with the tooltip's content and two additional classes - `with-tooltip` and `tooltip--[position]` on generated DOM element, ready for use. But before we can use the tooltip, we need to register our directive within the application. We will do this in the next section.

## Registering and using the tooltip in the application

In `main.js`, we need to import `TooltipDirective` and register it with the `app` instance using the `app.directive` method, as follows:

```javascript
import { createApp } from 'vue'
import App from './App.vue'
import { TooltipDirective } from './directives/TooltipDirective'

const app = createApp(App)

app.directive('tooltip', TooltipDirective )

app.mount('#app')
```

Vue will create the relevant directive with a `v-` prefix. We can now use our tooltip directive in any element with the name `v-tooltip` as follows:

```html
<button v-tooltip="{ text: 'I am the top tooltip', position: 'top' }">Top</button>
```

And in the browser, we will see the `button` element generated with the following HTML:

```html
<button data-tooltip="Im the top tooltip" class="with-tooltip tooltip--top">Top</button>
```

However, no tooltip will be displayed when hovering or focusing on the element. We need to add some CSS to our tooltip classes, which we will do next.

## Adding the tooltip effect with CSS

We add a new `assets/tooltip.css` file to our application and import it into our `main.js` file to have it included in our application:

```js
/**main.js */
import './assets/tooltip.css';
```

In the `tooltip.css` file, we add the following CSS rules to style our tooltip:

```css
.with-tooltip {
    position: relative;
}

.with-tooltip::before {
    content: attr(data-tooltip);
    opacity: 0;
    position: absolute;
    transition: opacity 2s;
    color: #ffffff;
    text-align: center;
    padding: 5px;
    border-radius: 2px;
    min-width: 120px;
    background: #5e5d5d;
    pointer-events: none;
    z-index: 1;
}

.with-tooltip::after {
    transition: opacity 2s;
    opacity: 0;
    content: "";
    border-width: 5px;
    border-style: solid;
    border-color: #5e5d5d transparent transparent transparent;
    position: absolute;
    z-index: 1;
    pointer-events: none;
}
```

Here we set the position of the target element to `relative` to allow the positioning of the tooltip to alignment with it. For the tooltip, we use the `::before` pseudo-element to display the tooltip text and the `::after` pseudo-element to display the tooltip arrow. We also set some CSS properties to ensure the appearance of the tooltip, such as the following:

* `opacity: 0` to both elements to hide them by default, 
* `z-index: 1` to ensure the tooltip is always on top of other elements, and
* `pointer-events: none` to ensure the browser won't catch any event, including hover and focus on the `::before` and `::after` elements.
* `content: attr(data-tooltip)` to set the tooltip text in `::before` to the value of the `data-tooltip` attribute using the `attr()`.
* `content: ""` to empty the `::after` element.

We also set the `opacity: 1` to both elements when the user hovers or focuses on the target element, as follows:

```css
.with-tooltip:hover::before, 
.with-tooltip:hover::after, 
.with-tooltip:focus::before, 
.with-tooltip:focus::after {
    opacity: 1;
}
```

And finally, we will set the position of the tooltip and the arrow, depending on the position of the tooltip, using the `::before` and `::after` pseudo-elements of the classes `tooltip--top`, `tooltip--bottom`, `tooltip--left`, and `tooltip--right` as below:

```css

.tooltip--top::before {
    inset-block-end: 120%;
    inset-inline-start: 50%;
    margin-inline-start: -60px;
}

.tooltip--bottom::before {
    inset-block-start: 120%;
    inset-inline-start: 50%;
    margin-inline-start: -60px;
}

.tooltip--left::before {
    inset-block-end: 0%;
    inset-inline-end: 120%;
    min-height: 100%;
}

.tooltip--right::before {
    inset-block-end: 0%;
    inset-inline-start: 120%;
    min-height: 100%;
}

.tooltip--left::after {
    inset-block-start: 25%;
    inset-inline-start: -20%;
    border-color: transparent transparent transparent #5e5d5d;
}

.tooltip--right::after {
    inset-block-start: 25%;
    inset-inline-end: -20%;
    border-color: transparent #5e5d5d transparent transparent;
}

.tooltip--top::after {
    inset-block-start: -20%;
    inset-inline-start: 40%;
    border-color: #5e5d5d transparent transparent  transparent;
}

.tooltip--bottom::after {
    inset-block-end: -20%;
    inset-inline-start: 40%;
    border-color: transparent transparent #5e5d5d transparent;
}
```

Note we use `inset-block` and `inset-inline` CSS logical properties to align the tooltip and the arrow with the target element. Refer to the [previous tutorial](https://dev.to/mayashavin/build-an-accessible-tooltip-component-efficiently-with-css-and-vue-1p27#position-the-tooltip-with-css-and-props) to learn more about logical properties and how we create the arrow tip.

Our button element will now have a tooltip when hovering or focusing on it, as seen in the below screenshot:

<img src="https://res.cloudinary.com/mayashavin/video/upload/q_auto/v1677062739/articles/tooltip/tooltip_hover.gif" loading="lazy" alt="Tooltip displays on hovering" class="mx-auto"/>

Straightforward enough. We can now play around with the tooltip's position by changing the `position` property of the value passed to the directive. However, it can be tedious to write the `position` property every time we use the directive. To make it cleaner and more organized, we can use the directive modifiers, and we will see how to do that next.

## Using directive modifiers for the position

We can define the position as the directive's modifiers, denoting with the syntax `v-tooltip.[position]`, like in the following example:

```html
    <button v-tooltip.top="'I am the top tooltip'">Top</button>
```

Vue will convert the modifiers into a `binding.modifiers` object, where each property is the name of the modifier, and its value is `true`. In the above example, the `modifiers` object will be `{ top: true }`.

In our `TooltipDirective.js`, we can implement a function to return the suitable position class based on the `binding.modifiers` object, as follows:

```js
function getPositionClass(modifiers) {
    if (modifiers.top) {
        return 'top';
    } else if (modifiers.bottom) {
        return 'bottom';
    } else if (modifiers.left) {
        return 'left';
    } else if (modifiers.right) {
        return 'right';
    } 
        
    return 'top';
}
```

And we can refactor `TooltipDirective.js` to use the `getPositionClass()` function like in the following code:

```js
  el.setAttribute("data-tooltip", binding.value?.text || binding.value);
  el.classList.add("with-tooltip");
  
  const position = binding.value.position || getPositionClass(binding.modifiers);
  el.classList.add(`tooltip--${position}`);
```

Note that we now get the text value from both `binding.value.text` and `binding.value` to allow the user to pass the text value as an object or a string. 

That's it. We can use our tooltip directive in favor of the modifiers, as in the below example:

```html
    <button v-tooltip.top="'I am the top tooltip'">Top</button>
    <button v-tooltip.bottom="'I am the bottom tooltip'">Bottom</button>
    <button v-tooltip.right="'I am the right tooltip'">Right</button>
    <button v-tooltip.left="'I am the left tooltip'">Left</button>
```

And the result will be as same as [the previous approach of creating a tooltip as a component]():

<img src="https://res.cloudinary.com/mayashavin/image/upload/q_auto,f_auto/v1677062739/articles/tooltip/tooltip_directives" loading="lazy" alt="Tooltip displays on hovering" class="mx-auto"/>

How about accessibility? It comes for free with our directive, without the need for `aria-describedby` or `role:tooltip` attributes since the tooltip text is on the same element. Since we place it on the `::before` pseudo-element, the screen reader will be able to read in the following order: tooltip text, the target element text, and the target element's role. Isn't it awesome?

---
## Summary

A custom directive is a great feature to create reusable add-ons at the low level when using components can be overkill. Also, by using the proper CSS element selector, we can add extra UI functionality, like a text tooltip for an element, with less code.

However, using the tooltip component approach can be an advantage for more complex scenarios where you want to display a tooltip with more than just text, like a paragraph or an image with some extra stylings. In such cases, I suggest combining both approaches, directive for simple tooltips and component for more complex ones.


👉 _If you'd like to catch up with me sometimes, follow me on [Twitter](https://twitter.com/MayaShavin) | [Facebook](https://www.facebook.com/mayashavin/)._

👉 _Learn about Vue with my new book [Learning Vue](). The early release is available now!_

Like this post or find it helpful? Share it 👇🏼 😉